diff --git a/Cargo.lock b/Cargo.lock index a9a57a88d2..813cfc97b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1700,6 +1700,7 @@ dependencies = [ "itertools 0.10.5", "linked-hash-map", "lmdb-rkv", + "lmdb-rkv-sys", "num", "num-derive", "num-rational", diff --git a/executor/evm/src/block_hash.rs b/executor/evm/src/block_hash.rs index e565e190e0..975cbade01 100644 --- a/executor/evm/src/block_hash.rs +++ b/executor/evm/src/block_hash.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use casper_storage::block_store::{ - lmdb::IndexedLmdbBlockStore, types::BlockHeight, BlockStoreError, BlockStoreProvider, - DataReader, + lmdb::LmdbBlockStore, types::BlockHeight, BlockStoreError, BlockStoreProvider, DataReader, }; use casper_types::{BlockHash, BlockHeader}; @@ -42,12 +41,12 @@ impl BlockHashProvider for NoBlockHashProvider { /// Block hash provider backed by Casper's indexed LMDB block store. #[derive(Clone, Debug)] pub struct IndexedLmdbBlockHashProvider { - block_store: Arc, + block_store: Arc, } impl IndexedLmdbBlockHashProvider { /// Creates a block hash provider backed by `block_store`. - pub fn new(block_store: Arc) -> Self { + pub fn new(block_store: Arc) -> Self { Self { block_store } } } diff --git a/node/src/components/block_accumulator/tests.rs b/node/src/components/block_accumulator/tests.rs index 21fd8d98be..b13baf6b6b 100644 --- a/node/src/components/block_accumulator/tests.rs +++ b/node/src/components/block_accumulator/tests.rs @@ -187,10 +187,20 @@ impl Reactor for MockReactor { ) .unwrap(); - let storage = Storage::new( + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (storage_root, mut storage_block_store) = + storage::open_block_store(&storage_withdir, "test").unwrap(); + storage::prune_block_store( + &mut storage_block_store, + chainspec.hard_reset_to_start_of_era(), + protocol_version, + ) + .unwrap(); + let mut storage = Storage::new( &storage_withdir, - None, - ProtocolVersion::from_parts(1, 0, 0), + storage_root, + storage_block_store, + protocol_version, EraId::default(), "test", chainspec.transaction_config.max_ttl.into(), @@ -200,6 +210,7 @@ impl Reactor for MockReactor { TransactionConfig::default(), ) .unwrap(); + storage.initialize_for_test(); let reactor = MockReactor { storage, diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index ecc1c4bd3c..10e898c1ca 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -14,7 +14,6 @@ mod utils; use std::{ cmp::Ordering, - collections::BTreeMap, convert::TryInto, fmt::{self, Debug, Formatter}, path::Path, @@ -33,7 +32,7 @@ use casper_storage::{ data_access_layer::{ AddressableEntityRequest, AddressableEntityResult, BlockStore, DataAccessLayer, EntryPointExistsRequest, ExecutionResultsChecksumRequest, FlushRequest, FlushResult, - GenesisRequest, GenesisResult, TrieRequest, + GenesisRequest, GenesisResult, ProtocolUpgradeRequest, ProtocolUpgradeResult, TrieRequest, }, global_state::{ state::{lmdb::LmdbGlobalState, CommitProvider, StateProvider}, @@ -44,13 +43,13 @@ use casper_storage::{ tracking_copy::TrackingCopyError, }; use casper_types::{ - account::AccountHash, ActivationPoint, Chainspec, ChainspecRawBytes, ChainspecRegistry, - EntityAddr, EraId, Key, PublicKey, + account::AccountHash, ActivationPoint, Chainspec, ChainspecRawBytes, ChainspecRegistry, Digest, + EntityAddr, EraId, Key, ProtocolUpgradeConfig, }; use crate::{ components::{fetcher::FetchResponse, Component, ComponentState}, - contract_runtime::{types::EraPrice, utils::handle_protocol_upgrade}, + contract_runtime::types::EraPrice, effect::{ announcements::{ ContractRuntimeAnnouncement, FatalAnnouncement, MetaBlockAnnouncement, @@ -62,10 +61,7 @@ use crate::{ }, fatal, protocol::Message, - types::{ - BlockPayload, ExecutableBlock, FinalizedBlock, InternalEraReport, MetaBlockState, - TrieOrChunk, TrieOrChunkId, - }, + types::{TrieOrChunk, TrieOrChunkId}, NodeRng, }; pub(crate) use config::Config; @@ -307,6 +303,47 @@ impl ContractRuntime { result } + /// Commits a protocol upgrade against global state and flushes it to disk. + /// + /// The commit itself runs on the blocking thread-pool (via `run_intensive_task`), since it + /// can take a long time; this lets the caller bound the wait with a timeout instead of + /// stalling its task indefinitely. + pub(crate) async fn commit_protocol_upgrade( + &self, + upgrade_config: ProtocolUpgradeConfig, + ) -> Result { + debug!(?upgrade_config, "upgrade"); + let start = Instant::now(); + let upgrade_request = ProtocolUpgradeRequest::new(upgrade_config); + + let data_access_layer = Arc::clone(&self.data_access_layer); + let metrics = Arc::clone(&self.metrics); + run_intensive_task(move || { + let result = data_access_layer.protocol_upgrade(upgrade_request); + if result.is_success() { + info!("committed upgrade"); + metrics + .commit_upgrade + .observe(start.elapsed().as_secs_f64()); + let flush_req = FlushRequest::new(); + if let FlushResult::Failure(err) = data_access_layer.flush(flush_req) { + return Err(format!("{:?}", err)); + } + } + + match result { + ProtocolUpgradeResult::RootNotFound => { + Err("Root not found for protocol upgrade".to_string()) + } + ProtocolUpgradeResult::Failure(err) => Err(format!("{:?}", err)), + ProtocolUpgradeResult::Success { + post_state_hash, .. + } => Ok(post_state_hash), + } + }) + .await + } + /// Handles a contract runtime request. fn handle_contract_runtime_request( &mut self, @@ -546,91 +583,6 @@ impl ContractRuntime { } .ignore() } - ContractRuntimeRequest::UpdatePreState { new_pre_state } => { - let next_block_height = new_pre_state.next_block_height(); - self.set_execution_pre_state(new_pre_state); - let current_price = self.current_gas_price.gas_price(); - async move { - let block_header = match effect_builder - .get_highest_complete_block_header_from_storage() - .await - { - Some(header) - if header.is_switch_block() - && (header.height() + 1 == next_block_height) => - { - header - } - Some(_) => { - return fatal!( - effect_builder, - "Latest complete block is not a switch block to update state" - ) - .await; - } - None => { - return fatal!( - effect_builder, - "No complete block header found to update post upgrade state" - ) - .await; - } - }; - - let payload = BlockPayload::new( - BTreeMap::new(), - vec![], - Default::default(), - false, - current_price, - ); - - let finalized_block = FinalizedBlock::new( - payload, - Some(InternalEraReport::default()), - block_header.timestamp(), - block_header.next_block_era_id(), - next_block_height, - PublicKey::System, - ); - - info!("Enqueuing block for execution post state refresh"); - - effect_builder - .enqueue_block_for_execution( - ExecutableBlock::from_finalized_block_and_transactions( - finalized_block, - vec![], - ), - MetaBlockState::new_not_to_be_gossiped(), - ) - .await; - } - .ignore() - } - ContractRuntimeRequest::DoProtocolUpgrade { - protocol_upgrade_config, - next_block_height, - parent_hash, - parent_seed, - } => { - let mut effects = Effects::new(); - let data_access_layer = Arc::clone(&self.data_access_layer); - let metrics = Arc::clone(&self.metrics); - effects.extend( - handle_protocol_upgrade( - effect_builder, - data_access_layer, - metrics, - *protocol_upgrade_config, - next_block_height, - parent_hash, - parent_seed, - ) - .ignore(), - ); - effects - } ContractRuntimeRequest::EnqueueBlockForExecution { executable_block, key_block_height_for_activation_point, @@ -847,6 +799,10 @@ impl ContractRuntime { pub(crate) fn current_era_price(&self) -> EraPrice { self.current_gas_price } + + pub(crate) fn current_gas_price(&self) -> u8 { + self.current_gas_price.gas_price() + } } impl Component for ContractRuntime diff --git a/node/src/components/contract_runtime/tests.rs b/node/src/components/contract_runtime/tests.rs index cfcfcd0987..714aaebf0d 100644 --- a/node/src/components/contract_runtime/tests.rs +++ b/node/src/components/contract_runtime/tests.rs @@ -124,9 +124,18 @@ impl reactor::Reactor for Reactor { } let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); - let storage = Storage::new( + let (storage_root, mut storage_block_store) = + storage::open_block_store(&storage_withdir, "test").unwrap(); + storage::prune_block_store( + &mut storage_block_store, + chainspec.hard_reset_to_start_of_era(), + chainspec.protocol_version(), + ) + .unwrap(); + let mut storage = Storage::new( &storage_withdir, - None, + storage_root, + storage_block_store, chainspec.protocol_version(), EraId::default(), "test", @@ -137,6 +146,7 @@ impl reactor::Reactor for Reactor { TransactionConfig::default(), ) .unwrap(); + storage.initialize_for_test(); let contract_runtime = ContractRuntime::new(storage.root_path(), &config.config, chainspec, registry)?; diff --git a/node/src/components/contract_runtime/utils.rs b/node/src/components/contract_runtime/utils.rs index a4789cb33b..280a046340 100644 --- a/node/src/components/contract_runtime/utils.rs +++ b/node/src/components/contract_runtime/utils.rs @@ -10,7 +10,6 @@ use std::{ fmt::Debug, ops::Range, sync::{Arc, Mutex}, - time::Instant, }; use tracing::{debug, error, info}; @@ -36,15 +35,10 @@ use crate::{ use casper_binary_port::SpeculativeExecutionResult; use casper_execution_engine::engine_state::{ExecutionEngineV1, WasmV1Result}; use casper_storage::{ - data_access_layer::{ - DataAccessLayer, FlushRequest, FlushResult, ProtocolUpgradeRequest, ProtocolUpgradeResult, - TransferResult, - }, - global_state::state::{lmdb::LmdbGlobalState, CommitProvider, StateProvider}, -}; -use casper_types::{ - BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig, Transaction, + data_access_layer::{DataAccessLayer, TransferResult}, + global_state::state::lmdb::LmdbGlobalState, }; +use casper_types::{BlockHash, Chainspec, EraId, Gas, Key, Transaction}; /// Maximum number of resource intensive tasks that can be run in parallel. /// @@ -493,75 +487,6 @@ pub(super) async fn exec_and_check_next( } } -pub(super) async fn handle_protocol_upgrade( - effect_builder: EffectBuilder, - data_access_layer: Arc>, - metrics: Arc, - upgrade_config: ProtocolUpgradeConfig, - next_block_height: u64, - parent_hash: BlockHash, - parent_seed: Digest, -) where - REv: From - + From - + From - + From - + From - + Send, -{ - debug!(?upgrade_config, "upgrade"); - let start = Instant::now(); - let upgrade_request = ProtocolUpgradeRequest::new(upgrade_config); - - let result = run_intensive_task(move || { - let result = data_access_layer.protocol_upgrade(upgrade_request); - if result.is_success() { - info!("committed upgrade"); - metrics - .commit_upgrade - .observe(start.elapsed().as_secs_f64()); - let flush_req = FlushRequest::new(); - if let FlushResult::Failure(err) = data_access_layer.flush(flush_req) { - return Err(format!("{:?}", err)); - } - } - - Ok(result) - }) - .await; - - match result { - Err(error_msg) => { - // The only way this happens is if there is a problem in the flushing. - error!(%error_msg, ":Error in post upgrade flush"); - fatal!(effect_builder, "{}", error_msg).await; - } - Ok(result) => match result { - ProtocolUpgradeResult::RootNotFound => { - let error_msg = "Root not found for protocol upgrade"; - fatal!(effect_builder, "{}", error_msg).await; - } - ProtocolUpgradeResult::Failure(err) => { - fatal!(effect_builder, "{:?}", err).await; - } - ProtocolUpgradeResult::Success { - post_state_hash, .. - } => { - let post_upgrade_state = ExecutionPreState::new( - next_block_height, - post_state_hash, - parent_hash, - parent_seed, - ); - - effect_builder - .update_contract_runtime_state(post_upgrade_state) - .await - } - }, - } -} - fn generate_range_by_index( highest_era: u64, batch_size: u64, diff --git a/node/src/components/fetcher/tests.rs b/node/src/components/fetcher/tests.rs index 4807d80a9a..9b2a230e2f 100644 --- a/node/src/components/fetcher/tests.rs +++ b/node/src/components/fetcher/tests.rs @@ -283,10 +283,20 @@ impl ReactorTrait for Reactor { ) -> Result<(Self, Effects), Self::Error> { let network = InMemoryNetwork::::new(event_queue, rng); - let storage = Storage::new( - &WithDir::new(cfg.temp_dir.path(), cfg.storage_config), + let storage_with_dir = WithDir::new(cfg.temp_dir.path(), cfg.storage_config); + let (storage_root, mut storage_block_store) = + storage::open_block_store(&storage_with_dir, &chainspec.network_config.name).unwrap(); + storage::prune_block_store( + &mut storage_block_store, chainspec.hard_reset_to_start_of_era(), chainspec.protocol_config.version, + ) + .unwrap(); + let mut storage = Storage::new( + &storage_with_dir, + storage_root, + storage_block_store, + chainspec.protocol_config.version, chainspec.protocol_config.activation_point.era_id(), &chainspec.network_config.name, chainspec.transaction_config.max_ttl.into(), @@ -296,6 +306,7 @@ impl ReactorTrait for Reactor { TransactionConfig::default(), ) .unwrap(); + storage.initialize_for_test(); let fake_transaction_acceptor = FakeTransactionAcceptor::new(); let transaction_fetcher = diff --git a/node/src/components/gossiper/tests.rs b/node/src/components/gossiper/tests.rs index 55cfbffa19..54744fd29c 100644 --- a/node/src/components/gossiper/tests.rs +++ b/node/src/components/gossiper/tests.rs @@ -150,7 +150,7 @@ impl reactor::Reactor for Reactor { fn new( config: Self::Config, - _chainspec: Arc, + chainspec: Arc, _chainspec_raw_bytes: Arc, _network_identity: NetworkIdentity, registry: &Registry, @@ -159,10 +159,20 @@ impl reactor::Reactor for Reactor { ) -> Result<(Self, Effects), Self::Error> { let (storage_config, storage_tempdir) = storage::Config::new_for_tests(1); let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); - let storage = Storage::new( + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (storage_root, mut storage_block_store) = + storage::open_block_store(&storage_withdir, "test").unwrap(); + storage::prune_block_store( + &mut storage_block_store, + chainspec.hard_reset_to_start_of_era(), + protocol_version, + ) + .unwrap(); + let mut storage = Storage::new( &storage_withdir, - None, - ProtocolVersion::from_parts(1, 0, 0), + storage_root, + storage_block_store, + protocol_version, EraId::default(), "test", MAX_TTL.into(), @@ -172,6 +182,7 @@ impl reactor::Reactor for Reactor { TransactionConfig::default(), ) .unwrap(); + storage.initialize_for_test(); let fake_transaction_acceptor = FakeTransactionAcceptor::new(); let transaction_gossiper = Gossiper::<{ Transaction::ID_IS_COMPLETE_ITEM }, _>::new( diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index 318bcb16be..885a686715 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -41,7 +41,7 @@ mod tests; mod utils; use casper_storage::block_store::{ - lmdb::{IndexedLmdbBlockStore, LmdbBlockStore}, + lmdb::LmdbBlockStore, types::{ ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockHeight, BlockTransfers, LatestSwitchBlock, StateStore, StateStoreKey, Tip, TransactionFinalizedApprovals, @@ -68,7 +68,7 @@ use casper_types::{ execution::{execution_result_v1, ExecutionResult, ExecutionResultV1}, Approval, ApprovalsHash, AvailableBlockRange, Block, BlockBody, BlockHash, BlockHeader, BlockHeaderWithSignatures, BlockSignatures, BlockSignaturesV1, BlockSignaturesV2, BlockV2, - ChainNameDigest, DeployHash, EraId, ExecutionInfo, FinalitySignature, ProtocolVersion, + ChainNameDigest, DeployHash, Digest, EraId, ExecutionInfo, FinalitySignature, ProtocolVersion, Timestamp, Transaction, TransactionConfig, TransactionHash, TransactionId, Transfer, U512, }; use datasize::DataSize; @@ -80,7 +80,7 @@ use tracing::{debug, error, info, warn}; use crate::{ components::{ fetcher::{FetchItem, FetchResponse}, - Component, + Component, ComponentState, InitializedComponent, }, effect::{ announcements::FatalAnnouncement, @@ -126,8 +126,8 @@ const STORAGE_FILES: [&str; 5] = [ pub struct Storage { /// Storage location. root: PathBuf, - /// Block store - pub(crate) block_store: IndexedLmdbBlockStore, + /// Block store. + block_store: LmdbBlockStore, /// Runs of completed blocks known in storage. completed_blocks: DisjointSequences, /// The activation point era of the current protocol version. @@ -153,6 +153,13 @@ pub struct Storage { transaction_config: TransactionConfig, /// The utilization of blocks. utilization_tracker: BTreeMap>, + /// Component initialization state. + state: ComponentState, + /// The protocol version this node is running. + #[data_size(skip)] + protocol_version: ProtocolVersion, + /// Whether a force resync was requested. + force_resync: bool, } #[allow(clippy::large_enum_variant)] @@ -193,41 +200,82 @@ where _rng: &mut NodeRng, event: Self::Event, ) -> Effects { - let result = match event { - Event::StorageRequest(req) => self.handle_storage_request(*req), - Event::NetRequestIncoming(ref incoming) => { - match self.handle_net_request_incoming::(effect_builder, incoming) { - Ok(effects) => Ok(effects), - Err(GetRequestError::Fatal(fatal_error)) => Err(fatal_error), - Err(ref other_err) => { - warn!( - sender=%incoming.sender, - err=display_error(other_err), - "error handling net request" + match &self.state { + ComponentState::Fatal(msg) => { + error!( + msg, + "should not handle this event when this component has fatal error" + ); + Effects::new() + } + ComponentState::Uninitialized => { + warn!(?event, "uninitialized component received event"); + Effects::new() + } + ComponentState::Initializing => match event { + Event::Initialize => match self.do_initialize() { + Ok(()) => { + >::set_state( + self, + ComponentState::Initialized, ); - // We could still send the requester a "not found" message, and could do - // so even in the fatal case, but it is safer to not do so at the - // moment, giving less surface area for possible amplification attacks. - Ok(Effects::new()) + Effects::new() } + Err(err) => fatal!(effect_builder, "storage error: {}", err).ignore(), + }, + _ => { + warn!( + ?event, + "initializing component received non-Initialize event" + ); + Effects::new() } - } - Event::MarkBlockCompletedRequest(req) => self.handle_mark_block_completed_request(req), - Event::MakeBlockExecutableRequest(req) => { - let ret = self.make_executable_block(&req.block_hash); - match ret { - Ok(maybe) => Ok(req.responder.respond(maybe).ignore()), - Err(err) => Err(err), + }, + ComponentState::Initialized => { + let result = match event { + Event::Initialize => { + info!("Storage: skipping initialization, already initialized"); + Ok(Effects::new()) + } + Event::StorageRequest(req) => self.handle_storage_request(*req), + Event::NetRequestIncoming(ref incoming) => { + match self.handle_net_request_incoming::(effect_builder, incoming) { + Ok(effects) => Ok(effects), + Err(GetRequestError::Fatal(fatal_error)) => Err(fatal_error), + Err(ref other_err) => { + warn!( + sender=%incoming.sender, + err=display_error(other_err), + "error handling net request" + ); + // We could still send the requester a "not found" message, and + // could do so even in the fatal case, but it is safer to not do + // so at the moment, giving less surface area for possible + // amplification attacks. + Ok(Effects::new()) + } + } + } + Event::MarkBlockCompletedRequest(req) => { + self.handle_mark_block_completed_request(req) + } + Event::MakeBlockExecutableRequest(req) => { + let ret = self.make_executable_block(&req.block_hash); + match ret { + Ok(maybe) => Ok(req.responder.respond(maybe).ignore()), + Err(err) => Err(err), + } + } + }; + + // Any error is turned into a fatal effect, the component itself does not panic. + // Note that we are dropping a lot of responders this way, but since we are + // crashing with fatal anyway, it should not matter. + match result { + Ok(effects) => effects, + Err(err) => fatal!(effect_builder, "storage error: {}", err).ignore(), } } - }; - - // Any error is turned into a fatal effect, the component itself does not panic. Note that - // we are dropping a lot of responders this way, but since we are crashing with fatal - // anyway, it should not matter. - match result { - Ok(effects) => effects, - Err(err) => fatal!(effect_builder, "storage error: {}", err).ignore(), } } @@ -236,12 +284,160 @@ where } } +impl InitializedComponent for Storage +where + REv: From + From> + Send, +{ + fn state(&self) -> &ComponentState { + &self.state + } + + fn set_state(&mut self, new_state: ComponentState) { + info!( + ?new_state, + name = >::name(self), + "component state changed" + ); + self.state = new_state; + } +} + +/// Opens (and, if necessary, builds the disk-backed indexes of) a node's block store. +pub fn open_block_store( + cfg: &WithDir, + network_name: &str, +) -> Result<(PathBuf, LmdbBlockStore), FatalStorageError> { + let config = cfg.value(); + + // Create the database directory. + let mut root = cfg.with_dir(config.path.clone()); + let network_subdir = root.join(network_name); + + if !network_subdir.exists() { + fs::create_dir_all(&network_subdir).map_err(|err| { + FatalStorageError::CreateDatabaseDirectory(network_subdir.clone(), err) + })?; + } + + if should_move_storage_files_to_network_subdir(&root, &STORAGE_FILES)? { + move_storage_files_to_network_subdir(&root, &network_subdir, &STORAGE_FILES)?; + } + + root = network_subdir; + + // Calculate the upper bound for the memory map that is potentially used. + let total_size = config + .max_block_store_size + .saturating_add(config.max_deploy_store_size) + .saturating_add(config.max_deploy_metadata_store_size); + + let mut block_store = LmdbBlockStore::new(root.as_path(), total_size)?; + block_store.init()?; + + Ok((root, block_store)) +} + +/// Performs the chainspec-driven hard-reset prune: deletes every stored block (and its body, +/// execution results, and index entries) at or after `hard_reset_to_start_of_era` that isn't +/// from the current protocol version. No-op if `hard_reset_to_start_of_era` is `None`. +pub fn prune_block_store( + block_store: &mut LmdbBlockStore, + hard_reset_to_start_of_era: Option, + protocol_version: ProtocolVersion, +) -> Result<(), FatalStorageError> { + let Some(invalid_era) = hard_reset_to_start_of_era else { + return Ok(()); + }; + + info!("pruning block store"); + + let tip_height = { + let ro_txn = block_store.checkout_ro()?; + match DataReader::::read(&ro_txn, Tip)? { + Some(header) => header.height(), + None => { + info!("block store is empty, nothing to prune"); + return Ok(()); + } + } + }; + let total_headers = tip_height + 1; + let progress_step = (total_headers / 20).max(1); + + // First pass (read-only): scan every header to decide which blocks to delete, and whether + // each body hash referenced along the way is still needed by at least one retained block + // (bodies are only ever deleted once no retained header references them any more). + let mut blocks_to_delete = Vec::new(); + let mut body_hash_retained: HashMap = HashMap::new(); + { + let ro_txn = block_store.checkout_ro()?; + for height in 0..=tip_height { + if height % progress_step == 0 { + info!( + percent_complete = (height * 100 / total_headers.max(1)), + height, total_headers, "pruning block store: scanning" + ); + } + let header: BlockHeader = match ro_txn.read(height)? { + Some(header) => header, + None => continue, + }; + + // Retain blocks from eras before the hard reset era, and blocks after this era if + // they are from the current protocol version (as otherwise a node restart would + // purge them again, despite them being valid). + let should_retain = + header.era_id() < invalid_era || header.protocol_version() == protocol_version; + + let retained = body_hash_retained + .entry(*header.body_hash()) + .or_insert(false); + *retained = *retained || should_retain; + + if !should_retain { + blocks_to_delete.push((header.block_hash(), header.height(), header.era_id())); + } + } + } + + if blocks_to_delete.is_empty() { + info!("block store pruning complete: nothing to prune"); + return Ok(()); + } + let blocks_to_delete_count = blocks_to_delete.len(); + + // Second pass (read-write): delete the execution results and the block itself for each + // block being pruned -- in that order, since deleting the execution results needs to read + // the block (header + body) to find its transaction hashes -- then purge any block body no + // longer referenced by a retained block. + { + let mut rw_txn = block_store.checkout_rw()?; + for (block_hash, block_height, era_id) in blocks_to_delete { + DataWriter::::delete( + &mut rw_txn, + BlockHashHeightAndEra::new(block_hash, block_height, era_id), + )?; + DataWriter::::delete(&mut rw_txn, block_hash)?; + } + for (body_hash, retained) in body_hash_retained { + if !retained { + DataWriter::::delete(&mut rw_txn, body_hash)?; + } + } + rw_txn.commit()?; + } + + info!(blocks_to_delete_count, "block store pruning complete"); + Ok(()) +} + impl Storage { - /// Creates a new storage component. + /// Ctor and init #[allow(clippy::too_many_arguments)] pub fn new( cfg: &WithDir, - hard_reset_to_start_of_era: Option, + root: PathBuf, + block_store: LmdbBlockStore, protocol_version: ProtocolVersion, activation_era: EraId, network_name: &str, @@ -252,38 +448,11 @@ impl Storage { transaction_config: TransactionConfig, ) -> Result { let config = cfg.value(); - - // Create the database directory. - let mut root = cfg.with_dir(config.path.clone()); - let network_subdir = root.join(network_name); - - if !network_subdir.exists() { - fs::create_dir_all(&network_subdir).map_err(|err| { - FatalStorageError::CreateDatabaseDirectory(network_subdir.clone(), err) - })?; - } - - if should_move_storage_files_to_network_subdir(&root, &STORAGE_FILES)? { - move_storage_files_to_network_subdir(&root, &network_subdir, &STORAGE_FILES)?; - } - - root = network_subdir; - - // Calculate the upper bound for the memory map that is potentially used. - let total_size = config - .max_block_store_size - .saturating_add(config.max_deploy_store_size) - .saturating_add(config.max_deploy_metadata_store_size); - - let block_store = LmdbBlockStore::new(root.as_path(), total_size)?; - let indexed_block_store = - IndexedLmdbBlockStore::new(block_store, hard_reset_to_start_of_era, protocol_version)?; - let metrics = registry.map(Metrics::new).transpose()?; - let mut component = Self { + Ok(Self { root, - block_store: indexed_block_store, + block_store, completed_blocks: Default::default(), activation_era, key_block_height_for_activation_point: None, @@ -295,10 +464,16 @@ impl Storage { metrics, chain_name_hash: ChainNameDigest::from_chain_name(network_name), transaction_config, - }; + state: ComponentState::Uninitialized, + protocol_version, + force_resync, + }) + } - if force_resync { - let force_resync_file_path = component.root_path().join(FORCE_RESYNC_FILE_NAME); + /// Performs completed-blocks bookkeeping (and force-resync marker handling, if configured). + fn do_initialize(&mut self) -> Result<(), FatalStorageError> { + if self.force_resync { + let force_resync_file_path = self.root_path().join(FORCE_RESYNC_FILE_NAME); // Check if resync is already in progress. Force resync will kick // in only when the marker file didn't exist before. // Use `OpenOptions::create_new` to atomically check for the file @@ -313,10 +488,10 @@ impl Storage { // is now created, initialize force resync. info!("initializing force resync"); // Default `storage.completed_blocks`. - component.completed_blocks = Default::default(); - component.persist_completed_blocks()?; + self.completed_blocks = Default::default(); + self.persist_completed_blocks()?; // Exit the initialization function early. - return Ok(component); + return Ok(()); } Err(io_err) if io_err.kind() == ErrorKind::AlreadyExists => { info!("skipping force resync as marker file exists"); @@ -332,7 +507,7 @@ impl Storage { } { - let ro_txn = component.block_store.checkout_ro()?; + let ro_txn = self.block_store.checkout_ro()?; let maybe_state_store: Option> = ro_txn.read(StateStoreKey::new( Cow::Borrowed(COMPLETED_BLOCKS_STORAGE_KEY), ))?; @@ -349,7 +524,7 @@ impl Storage { sequences.clear(); } - component.completed_blocks = sequences; + self.completed_blocks = sequences; } None => { // No state so far. We can make the following observations: @@ -375,8 +550,10 @@ impl Storage { for height in (0..=highest_block_header.height()).rev() { let maybe_header: Option = ro_txn.read(height)?; match maybe_header { - Some(header) if header.protocol_version() < protocol_version => { - component.completed_blocks = + Some(header) + if header.protocol_version() < self.protocol_version => + { + self.completed_blocks = DisjointSequences::new(Sequence::new(0, header.height())); break; } @@ -387,9 +564,9 @@ impl Storage { } } } - component.persist_completed_blocks()?; - component.warm_up_utilization_tracker()?; - Ok(component) + self.persist_completed_blocks()?; + self.warm_up_utilization_tracker()?; + Ok(()) } /// Assume: @@ -671,9 +848,10 @@ impl Storage { StorageRequest::GetApprovalsHashes { block_hash, responder, - } => responder - .respond(self.block_store.checkout_ro()?.read(block_hash)?) - .ignore(), + } => { + let maybe_item = self.block_store.checkout_ro()?.read(block_hash)?; + responder.respond(maybe_item).ignore() + } StorageRequest::GetHighestCompleteBlock { responder } => responder .respond(self.get_highest_complete_block()?) .ignore(), @@ -2342,6 +2520,16 @@ fn successful_transfers(execution_result: &ExecutionResult) -> Vec { // only ever be used when writing tests. #[cfg(test)] impl Storage { + /// Drives this component through its `InitializedComponent` initialization step, mirroring + /// what the reactor's `initialize_next_component` does at startup. Test harnesses across the + /// crate that construct a `Storage` directly (rather than via the full reactor) must call + /// this before issuing any `StorageRequest`s against it. + pub(crate) fn initialize_for_test(&mut self) { + self.do_initialize() + .expect("storage initialization should succeed"); + self.state = ComponentState::Initialized; + } + /// Directly returns a transaction with finalized approvals from internal store. /// /// # Panics diff --git a/node/src/components/storage/event.rs b/node/src/components/storage/event.rs index 8f02790c9a..2b54c45e9d 100644 --- a/node/src/components/storage/event.rs +++ b/node/src/components/storage/event.rs @@ -16,6 +16,8 @@ const_assert!(_STORAGE_EVENT_SIZE <= 32); #[derive(Debug, From, Serialize)] #[repr(u8)] pub(crate) enum Event { + /// Initializing event + Initialize, /// Storage request. #[from] StorageRequest(Box), @@ -32,6 +34,7 @@ pub(crate) enum Event { impl Display for Event { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { + Event::Initialize => write!(f, "initialize"), Event::StorageRequest(req) => req.fmt(f), Event::NetRequestIncoming(incoming) => incoming.fmt(f), Event::MarkBlockCompletedRequest(req) => req.fmt(f), diff --git a/node/src/components/storage/tests.rs b/node/src/components/storage/tests.rs index 3c0b482337..6623309070 100644 --- a/node/src/components/storage/tests.rs +++ b/node/src/components/storage/tests.rs @@ -33,8 +33,8 @@ use casper_types::{ use tempfile::tempdir; use super::{ - move_storage_files_to_network_subdir, should_move_storage_files_to_network_subdir, Config, - Storage, FORCE_RESYNC_FILE_NAME, + move_storage_files_to_network_subdir, open_block_store, prune_block_store, + should_move_storage_files_to_network_subdir, Config, Storage, FORCE_RESYNC_FILE_NAME, }; use crate::{ components::fetcher::{FetchItem, FetchResponse}, @@ -187,10 +187,17 @@ fn create_sync_leap_test_chain( /// Panics if setting up the storage fixture fails. fn storage_fixture(harness: &ComponentHarness) -> Storage { let cfg = new_config(harness); - Storage::new( - &WithDir::new(harness.tmp.path(), cfg), - None, - ProtocolVersion::from_parts(1, 0, 0), + let cfg = WithDir::new(harness.tmp.path(), cfg); + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (root, mut block_store) = + open_block_store(&cfg, "test").expect("could not open block store fixture"); + prune_block_store(&mut block_store, None, protocol_version) + .expect("could not prune block store fixture"); + let mut storage = Storage::new( + &cfg, + root, + block_store, + protocol_version, EraId::default(), "test", MAX_TTL.into(), @@ -199,7 +206,9 @@ fn storage_fixture(harness: &ComponentHarness) -> Storage { false, TransactionConfig::default(), ) - .expect("could not create storage component fixture") + .expect("could not create storage component fixture"); + storage.initialize_for_test(); + storage } /// Storage component test fixture. @@ -218,19 +227,33 @@ fn storage_fixture_from_parts( recent_era_count: Option, ) -> Storage { let cfg = new_config(harness); - Storage::new( - &WithDir::new(harness.tmp.path(), cfg), + let cfg = WithDir::new(harness.tmp.path(), cfg); + let network_name = network_name.unwrap_or("test"); + let protocol_version = protocol_version.unwrap_or(ProtocolVersion::V1_0_0); + let (root, mut block_store) = + open_block_store(&cfg, network_name).expect("could not open block store fixture"); + prune_block_store( + &mut block_store, hard_reset_to_start_of_era, - protocol_version.unwrap_or(ProtocolVersion::V1_0_0), + protocol_version, + ) + .expect("could not prune block store fixture"); + let mut storage = Storage::new( + &cfg, + root, + block_store, + protocol_version, EraId::default(), - network_name.unwrap_or("test"), + network_name, max_ttl.unwrap_or(MAX_TTL).into(), recent_era_count.unwrap_or(RECENT_ERA_COUNT), None, false, TransactionConfig::default(), ) - .expect("could not create storage component fixture from parts") + .expect("could not create storage component fixture from parts"); + storage.initialize_for_test(); + storage } /// Storage component test fixture with force resync enabled. @@ -241,10 +264,16 @@ fn storage_fixture_from_parts( /// /// Panics if setting up the storage fixture fails. fn storage_fixture_with_force_resync(cfg: &WithDir) -> Storage { - Storage::new( + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (root, mut block_store) = + open_block_store(cfg, "test").expect("could not open block store fixture"); + prune_block_store(&mut block_store, None, protocol_version) + .expect("could not prune block store fixture"); + let mut storage = Storage::new( cfg, - None, - ProtocolVersion::from_parts(1, 0, 0), + root, + block_store, + protocol_version, EraId::default(), "test", MAX_TTL.into(), @@ -253,7 +282,9 @@ fn storage_fixture_with_force_resync(cfg: &WithDir) -> Storage { true, TransactionConfig::default(), ) - .expect("could not create storage component fixture") + .expect("could not create storage component fixture"); + storage.initialize_for_test(); + storage } /// Storage component test fixture. @@ -1777,10 +1808,15 @@ fn should_create_subdir_named_after_network() { let cfg = new_config(&harness); let network_name = "test"; + let with_dir = WithDir::new(harness.tmp.path(), cfg.clone()); + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (root, mut block_store) = open_block_store(&with_dir, network_name).unwrap(); + prune_block_store(&mut block_store, None, protocol_version).unwrap(); let storage = Storage::new( - &WithDir::new(harness.tmp.path(), cfg.clone()), - None, - ProtocolVersion::from_parts(1, 0, 0), + &with_dir, + root, + block_store, + protocol_version, EraId::default(), network_name, MAX_TTL.into(), diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index 0d7705244c..d2f068fc82 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -1418,10 +1418,20 @@ impl reactor::Reactor for Reactor { let transaction_acceptor = TransactionAcceptor::new(Config::default(), Arc::clone(&chainspec), registry)?; - let storage = Storage::new( + let protocol_version = ProtocolVersion::from_parts(1, 0, 0); + let (storage_root, mut storage_block_store) = + storage::open_block_store(&storage_with_dir, "test").unwrap(); + storage::prune_block_store( + &mut storage_block_store, + chainspec.hard_reset_to_start_of_era(), + protocol_version, + ) + .unwrap(); + let mut storage = Storage::new( &storage_with_dir, - None, - ProtocolVersion::from_parts(1, 0, 0), + storage_root, + storage_block_store, + protocol_version, EraId::default(), "test", chainspec.transaction_config.max_ttl.into(), @@ -1431,6 +1441,7 @@ impl reactor::Reactor for Reactor { TransactionConfig::default(), ) .unwrap(); + storage.initialize_for_test(); let reactor = Reactor { storage, diff --git a/node/src/effect.rs b/node/src/effect.rs index fd33526dcc..ba09f3b1b8 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -134,8 +134,8 @@ use casper_types::{ Approval, AvailableBlockRange, Block, BlockHash, BlockHeader, BlockSignatures, BlockSynchronizerStatus, BlockV2, ChainspecRawBytes, DeployHash, Digest, EntityAddr, EraId, ExecutionInfo, FinalitySignature, FinalitySignatureId, FinalitySignatureV2, HashAddr, Key, - NextUpgrade, Package, PackageAddr, ProtocolUpgradeConfig, PublicKey, TimeDiff, Timestamp, - Transaction, TransactionHash, TransactionId, Transfer, U512, + NextUpgrade, Package, PackageAddr, PublicKey, TimeDiff, Timestamp, Transaction, + TransactionHash, TransactionId, Transfer, U512, }; use crate::{ @@ -152,7 +152,6 @@ use crate::{ network::{blocklist::BlocklistJustification, FromIncoming, NetworkInsights}, transaction_acceptor, }, - contract_runtime::ExecutionPreState, effect::announcements::NonExecutableBlockAnnouncement, failpoints::FailpointActivation, reactor::{main_reactor::ReactorState, EventQueueHandle, QueueKind}, @@ -961,18 +960,6 @@ impl EffectBuilder { .await; } - pub(crate) async fn update_contract_runtime_state(self, new_pre_state: ExecutionPreState) - where - REv: From, - { - self.event_queue - .schedule( - ContractRuntimeRequest::UpdatePreState { new_pre_state }, - QueueKind::ContractRuntime, - ) - .await; - } - /// Announces validators for upcoming era. pub(crate) async fn announce_upcoming_era_validators( self, @@ -1815,28 +1802,6 @@ impl EffectBuilder { .await; } - pub(crate) async fn enqueue_protocol_upgrade( - self, - upgrade_config: ProtocolUpgradeConfig, - next_block_height: u64, - parent_hash: BlockHash, - parent_seed: Digest, - ) where - REv: From, - { - self.event_queue - .schedule( - ContractRuntimeRequest::DoProtocolUpgrade { - protocol_upgrade_config: Box::new(upgrade_config), - next_block_height, - parent_hash, - parent_seed, - }, - QueueKind::Control, - ) - .await; - } - /// Checks whether the transactions included in the block exist on the network and that /// the block is valid. pub(crate) async fn validate_block( diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index c04a27d98e..4fea7eda52 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -34,8 +34,8 @@ use casper_types::{ execution::ExecutionResult, Approval, AvailableBlockRange, Block, BlockHash, BlockHeader, BlockSignatures, BlockSynchronizerStatus, BlockV2, ChainspecRawBytes, DeployHash, Digest, DisplayIter, EntityAddr, EraId, ExecutionInfo, FinalitySignature, FinalitySignatureId, - HashAddr, NextUpgrade, ProtocolUpgradeConfig, PublicKey, TimeDiff, Timestamp, Transaction, - TransactionHash, TransactionId, Transfer, + HashAddr, NextUpgrade, PublicKey, TimeDiff, Timestamp, Transaction, TransactionHash, + TransactionId, Transfer, }; use super::{AutoClosingResponder, GossipTarget, Responder}; @@ -53,7 +53,6 @@ use crate::{ network::NetworkInsights, transaction_acceptor, }, - contract_runtime::ExecutionPreState, reactor::main_reactor::ReactorState, types::{ appendable_block::AppendableBlock, BlockExecutionResultsOrChunk, @@ -890,15 +889,6 @@ pub(crate) enum ContractRuntimeRequest { era_id: EraId, responder: Responder>, }, - DoProtocolUpgrade { - protocol_upgrade_config: Box, - next_block_height: u64, - parent_hash: BlockHash, - parent_seed: Digest, - }, - UpdatePreState { - new_pre_state: ExecutionPreState, - }, } impl Display for ContractRuntimeRequest { @@ -993,23 +983,6 @@ impl Display for ContractRuntimeRequest { formatted_contract_hash, entry_point_name, state_root_hash ) } - ContractRuntimeRequest::DoProtocolUpgrade { - protocol_upgrade_config, - .. - } => { - write!( - formatter, - "execute protocol upgrade against config: {:?}", - protocol_upgrade_config - ) - } - ContractRuntimeRequest::UpdatePreState { new_pre_state } => { - write!( - formatter, - "Updating contract runtimes execution prestate: {:?}", - new_pre_state - ) - } } } } diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 9375fde4dd..872e4c3459 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -6,6 +6,7 @@ mod error; mod event; mod fetchers; mod memory_metrics; +mod protocol_upgrade; mod utils; mod catch_up; @@ -15,7 +16,6 @@ mod reactor_state; #[cfg(test)] mod tests; mod upgrade_shutdown; -mod upgrading_instruction; mod validate; use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; @@ -26,9 +26,10 @@ use prometheus::Registry; use tracing::{debug, error, info, warn}; use casper_binary_port::{LastProgress, NetworkName, Uptime}; +use casper_storage::block_store::{types::Tip, BlockStoreProvider, DataReader}; use casper_types::{ - bytesrepr, Block, BlockHash, BlockV2, Chainspec, ChainspecRawBytes, EraId, FinalitySignature, - FinalitySignatureV2, PublicKey, TimeDiff, Timestamp, Transaction, U512, + bytesrepr, Block, BlockHash, BlockHeader, BlockV2, Chainspec, ChainspecRawBytes, EraId, + FinalitySignature, FinalitySignatureV2, PublicKey, TimeDiff, Timestamp, Transaction, U512, }; #[cfg(test)] @@ -48,7 +49,7 @@ use crate::{ network::{self, GossipedAddress, Identity as NetworkIdentity, Network}, rest_server::RestServer, shutdown_trigger::{self, CompletedBlockInfo, ShutdownTrigger}, - storage::Storage, + storage::{self, Storage}, sync_leaper::SyncLeaper, transaction_acceptor::{self, TransactionAcceptor}, transaction_buffer::{self, TransactionBuffer}, @@ -77,7 +78,11 @@ use crate::{ reactor::{ self, event_queue_metrics::EventQueueMetrics, - main_reactor::{fetchers::Fetchers, upgrade_shutdown::SignatureGossipTracker}, + main_reactor::{ + fetchers::Fetchers, + protocol_upgrade::{commit_upgrade_if_needed, PendingImmediateSwitchBlock}, + upgrade_shutdown::SignatureGossipTracker, + }, EventQueueHandle, QueueKind, }, types::{ @@ -205,6 +210,18 @@ pub(crate) struct MainReactor { prevent_validator_shutdown: bool, force_catchup: bool, + + /// Set when a protocol upgrade has been committed against global state but its immediate + /// switch block hasn't yet been produced, signed, and gossiped -- deferred until the node + /// can actually reach peers (see `MainReactor::maybe_finish_pending_upgrade`). + pending_immediate_switch_block: Option, + + /// Set to the current time when the immediate switch block for a protocol upgrade is + /// enqueued for execution, and cleared once the upgrade is observed to be complete (i.e. + /// `should_commit_upgrade` becomes false again, meaning the local tip has advanced past the + /// switch block). If it takes longer than `upgrade_timeout` for that to happen, the reactor + /// bails out fatally rather than waiting forever. + upgrade_started_at: Option, } impl reactor::Reactor for MainReactor { @@ -1115,10 +1132,53 @@ impl reactor::Reactor for MainReactor { let storage_config = WithDir::new(&root_dir, config.storage.clone()); - let hard_reset_to_start_of_era = chainspec.hard_reset_to_start_of_era(); + // Open (and, if necessary, build the disk-backed indexes of) the block store as early + // as possible, before anything else touches disk-backed state. + let (storage_root, mut block_store) = + storage::open_block_store(&storage_config, &chainspec.network_config.name)?; + storage::prune_block_store( + &mut block_store, + chainspec.hard_reset_to_start_of_era(), + protocol_version, + )?; + + let contract_runtime = ContractRuntime::new( + &storage_root, + &config.contract_runtime, + chainspec.clone(), + registry, + )?; + + // If our local tip (post-prune) is the activation point, commit the protocol upgrade + // synchronously now. The resulting immediate switch block is *not* produced yet: that's + // deferred until the node can actually sign and gossip it + // (see `MainReactor::maybe_finish_pending_upgrade`). + let local_tip: Option = { + let ro_txn = block_store + .checkout_ro() + .map_err(storage::FatalStorageError::from)?; + DataReader::::read(&ro_txn, Tip) + .map_err(storage::FatalStorageError::from)? + }; + + // config.node.skip_protocol_upgrade allows to skip the protocol upgrade. In this flow we + // rely that the node will sync_leap the immediate switch blocks from peers. + let pending_immediate_switch_block = if config.node.skip_protocol_upgrade { + None + } else { + commit_upgrade_if_needed( + &contract_runtime, + &chainspec, + &chainspec_raw_bytes, + local_tip.as_ref(), + config.node.upgrade_timeout, + )? + }; + let storage = Storage::new( &storage_config, - hard_reset_to_start_of_era, + storage_root, + block_store, protocol_version, chainspec.protocol_config.activation_point.era_id(), &chainspec.network_config.name, @@ -1129,13 +1189,6 @@ impl reactor::Reactor for MainReactor { chainspec.transaction_config.clone(), )?; - let contract_runtime = ContractRuntime::new( - storage.root_path(), - &config.contract_runtime, - chainspec.clone(), - registry, - )?; - let allow_handshake = config.node.sync_handling != SyncHandling::Isolated; let network = Network::new( @@ -1287,6 +1340,8 @@ impl reactor::Reactor for MainReactor { finality_signature_creation: true, prevent_validator_shutdown, force_catchup: false, + pending_immediate_switch_block, + upgrade_started_at: None, }; info!("MainReactor: instantiated"); diff --git a/node/src/reactor/main_reactor/catch_up.rs b/node/src/reactor/main_reactor/catch_up.rs index f98134a566..dd4615a393 100644 --- a/node/src/reactor/main_reactor/catch_up.rs +++ b/node/src/reactor/main_reactor/catch_up.rs @@ -29,7 +29,6 @@ pub(super) enum CatchUpInstruction { ShutdownForUpgrade, CaughtUp, CommitGenesis, - CommitUpgrade, } impl MainReactor { @@ -265,7 +264,7 @@ impl MainReactor { SyncInstruction::BlockSync { block_hash } => { Some(self.catch_up_block_sync(effect_builder, block_hash)) } - SyncInstruction::CaughtUp { .. } => self.catch_up_check_transition(), + SyncInstruction::CaughtUp { .. } => self.catch_up_check_transition(effect_builder), } } @@ -413,11 +412,42 @@ impl MainReactor { } } - fn catch_up_check_transition(&mut self) -> Option { - // we may be starting back up after a shutdown for upgrade; if so we need to - // commit upgrade now before proceeding further - if self.should_commit_upgrade() { - return Some(CatchUpInstruction::CommitUpgrade); + fn catch_up_check_transition( + &mut self, + effect_builder: EffectBuilder, + ) -> Option { + // We may be starting back up after a shutdown for upgrade -- i.e. `MainReactor::new` + // found the local tip already sitting at the pre-activation switch block and committed + // the upgrade synchronously before the reactor even existed. If so, `CatchUp` just needs + // to finish (sign + gossip) the resulting immediate switch block below. + // + // A node catching up through a *historical* activation point (a new node joining an + // already-upgraded network) does NOT commit the upgrade itself here: it acquires the + // post-upgrade chain the same way it acquires everything else during catch-up, via the + // block synchronizer fetching from peers. Verified empirically against `dev`: a joining + // node given a pre-upgrade trusted hash and left to sync forward through a live upgrade + // never invokes the local commit path (no "committing protocol upgrade" / "switch to + // Upgrading" log line), while the already-running nodes that restart into it do. + if self.pending_immediate_switch_block.is_some() { + // `CatchUp` is only reachable after the `Initialize` peer-gate has passed, so it's + // always safe to finish (sign + gossip) the pending upgrade block right away. + if let Some(effects) = self.maybe_finish_pending_upgrade(effect_builder) { + return Some(CatchUpInstruction::Do(Duration::ZERO, effects)); + } + } + // If we're still waiting for the upgrade's immediate switch block to land (i.e. to be + // executed, stored, and marked complete), bail out fatally rather than wait forever if + // that's taking longer than `upgrade_timeout`. + if let Some(started_at) = self.upgrade_started_at { + if !self.should_commit_upgrade() { + // the local tip has advanced past the switch block: the upgrade is complete. + self.upgrade_started_at = None; + } else if started_at.elapsed() > self.upgrade_timeout { + return Some(CatchUpInstruction::Fatal(format!( + "protocol upgrade did not complete within {}", + self.upgrade_timeout + ))); + } } // we may need to shutdown to go thru an upgrade if self.should_shutdown_for_upgrade() { diff --git a/node/src/reactor/main_reactor/control.rs b/node/src/reactor/main_reactor/control.rs index d62e8a034e..f2b63a5734 100644 --- a/node/src/reactor/main_reactor/control.rs +++ b/node/src/reactor/main_reactor/control.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{collections::BTreeMap, time::Duration}; use tracing::{debug, error, info, trace}; use casper_storage::data_access_layer::GenesisResult; @@ -9,15 +9,14 @@ use crate::{ binary_port, block_synchronizer::{self, BlockSynchronizerProgress}, contract_runtime::ExecutionPreState, - diagnostics_port, event_stream_server, network, rest_server, upgrade_watcher, + diagnostics_port, event_stream_server, network, rest_server, storage, upgrade_watcher, }, effect::{announcements::ControlAnnouncement, EffectBuilder, EffectExt, Effects}, fatal, reactor::main_reactor::{ catch_up::CatchUpInstruction, genesis_instruction::GenesisInstruction, - keep_up::KeepUpInstruction, upgrade_shutdown::UpgradeShutdownInstruction, - upgrading_instruction::UpgradingInstruction, utils, validate::ValidateInstruction, - MainEvent, MainReactor, ReactorState, + keep_up::KeepUpInstruction, upgrade_shutdown::UpgradeShutdownInstruction, utils, + validate::ValidateInstruction, MainEvent, MainReactor, ReactorState, }, types::{BlockPayload, ExecutableBlock, FinalizedBlock, InternalEraReport, MetaBlockState}, NodeRng, @@ -63,39 +62,38 @@ impl MainReactor { None => { if self.sync_handling.is_isolated() { // If node is "isolated" it doesn't care about peers - if let Err(msg) = self.refresh_contract_runtime() { - return ( - Duration::ZERO, - fatal!(effect_builder, "{}", msg).ignore(), - ); - } + let effects = match self + .refresh_contract_runtime_or_finish_upgrade(effect_builder) + { + Ok(effects) => effects, + Err(msg) => { + return ( + Duration::ZERO, + fatal!(effect_builder, "{}", msg).ignore(), + ) + } + }; self.state = ReactorState::KeepUp; - return (Duration::ZERO, Effects::new()); + return (Duration::ZERO, effects); } if false == self.net.has_sufficient_fully_connected_peers() { info!("Initialize: awaiting sufficient fully-connected peers"); return (initialization_logic_default_delay.into(), Effects::new()); } - if let Err(msg) = self.refresh_contract_runtime() { - return (Duration::ZERO, fatal!(effect_builder, "{}", msg).ignore()); - } + let effects = match self + .refresh_contract_runtime_or_finish_upgrade(effect_builder) + { + Ok(effects) => effects, + Err(msg) => { + return (Duration::ZERO, fatal!(effect_builder, "{}", msg).ignore()) + } + }; info!("Initialize: switch to CatchUp"); self.state = ReactorState::CatchUp; - (Duration::ZERO, Effects::new()) + (Duration::ZERO, effects) } } } - ReactorState::Upgrading => match self.upgrading_instruction() { - UpgradingInstruction::CheckLater(msg, wait) => { - debug!("Upgrading: {}", msg); - (wait, Effects::new()) - } - UpgradingInstruction::CatchUp => { - info!("Upgrading: switch to CatchUp"); - self.state = ReactorState::CatchUp; - (Duration::ZERO, Effects::new()) - } - }, ReactorState::CatchUp => match self.catch_up_instruction(effect_builder, rng) { CatchUpInstruction::Fatal(msg) => { (Duration::ZERO, fatal!(effect_builder, "{}", msg).ignore()) @@ -122,20 +120,6 @@ impl MainReactor { fatal!(effect_builder, "failed to commit genesis: {}", msg).ignore(), ), }, - CatchUpInstruction::CommitUpgrade => match self.commit_upgrade(effect_builder) { - Ok(effects) => { - info!("CatchUp: switch to Upgrading"); - self.block_synchronizer.purge(); - self.state = ReactorState::Upgrading; - self.last_progress = Timestamp::now(); - self.attempts = 0; - (Duration::ZERO, effects) - } - Err(msg) => ( - Duration::ZERO, - fatal!(effect_builder, "failed to commit upgrade: {}", msg).ignore(), - ), - }, CatchUpInstruction::CheckLater(msg, wait) => { debug!("CatchUp: {}", msg); (wait, Effects::new()) @@ -263,6 +247,15 @@ impl MainReactor { &mut self, effect_builder: EffectBuilder, ) -> Option> { + // storage must be ready before anything else touches disk-backed state (other + // components, e.g. transaction_buffer, read from it during their own init). + if let Some(effects) = utils::initialize_component( + effect_builder, + &mut self.storage, + MainEvent::Storage(storage::Event::Initialize), + ) { + return Some(effects); + } // open the diagnostic port first to make sure it can bind & to be responsive during init. if let Some(effects) = utils::initialize_component( effect_builder, @@ -434,52 +427,71 @@ impl MainReactor { } } - fn upgrading_instruction(&self) -> UpgradingInstruction { - UpgradingInstruction::should_commit_upgrade( - self.should_commit_upgrade(), - self.control_logic_default_delay.into(), - self.last_progress, - self.upgrade_timeout, + /// If a protocol upgrade has been committed and its immediate switch block hasn't yet been + /// produced, builds the effects to enqueue it for execution, which will get it signed (by + /// this validator, if applicable) and gossiped through the normal block-execution pipeline + /// (see `main_reactor::handle_meta_block`). + /// + /// The caller MUST NOT invoke this before the node can actually reach peers -- broadcasting a + /// finality signature to zero connected peers silently drops it with no retry (see + /// `network::broadcast_message_to_validators`). This is why callers only invoke it once the + /// existing `Initialize` peer-gate (`has_sufficient_fully_connected_peers`, or isolated mode) + /// has passed, or from within `CatchUp`, which is only reachable after that same gate. + pub(super) fn maybe_finish_pending_upgrade( + &mut self, + effect_builder: EffectBuilder, + ) -> Option> { + let pending = self.pending_immediate_switch_block.take()?; + self.upgrade_started_at = Some(Timestamp::now()); + self.contract_runtime + .set_execution_pre_state(ExecutionPreState::new( + pending.next_block_height(), + pending.post_state_hash(), + pending.parent_hash(), + pending.parent_seed(), + )); + + let current_price = self.contract_runtime.current_gas_price(); + let payload = BlockPayload::new( + BTreeMap::new(), + vec![], + Default::default(), + false, + current_price, + ); + let finalized_block = FinalizedBlock::new( + payload, + Some(InternalEraReport::default()), + pending.timestamp(), + pending.era_id(), + pending.next_block_height(), + PublicKey::System, + ); + + info!("producing immediate switch block after protocol upgrade"); + + Some( + effect_builder + .enqueue_block_for_execution( + ExecutableBlock::from_finalized_block_and_transactions(finalized_block, vec![]), + MetaBlockState::new_not_to_be_gossiped(), + ) + .ignore(), ) } - fn commit_upgrade( + /// Either finishes a pending protocol upgrade (producing its immediate switch block) or, if + /// none is pending, refreshes contract runtime's execution pre-state from the local tip as + /// usual. Used at the two points the reactor exits `ReactorState::Initialize`. + fn refresh_contract_runtime_or_finish_upgrade( &mut self, effect_builder: EffectBuilder, ) -> Result, String> { - let header = match self.get_local_tip_header()? { - Some(header) if header.is_switch_block() => header, - Some(_) => { - return Err("Latest complete block is not a switch block".to_string()); - } - None => { - return Err("No complete block found in storage".to_string()); - } - }; - - match self.chainspec.upgrade_config_from_parts( - *header.state_root_hash(), - header.protocol_version(), - self.chainspec.protocol_config.activation_point.era_id(), - self.chainspec_raw_bytes.clone(), - ) { - Ok(cfg) => { - let mut effects = Effects::new(); - let next_block_height = header.height() + 1; - effects.extend( - effect_builder - .enqueue_protocol_upgrade( - cfg, - next_block_height, - header.block_hash(), - *header.accumulated_seed(), - ) - .ignore(), - ); - Ok(effects) - } - Err(msg) => Err(msg), + if let Some(effects) = self.maybe_finish_pending_upgrade(effect_builder) { + return Ok(effects); } + self.refresh_contract_runtime()?; + Ok(Effects::new()) } pub(super) fn should_shutdown_for_upgrade(&self) -> bool { diff --git a/node/src/reactor/main_reactor/error.rs b/node/src/reactor/main_reactor/error.rs index 86a58a1a5b..4e0cb96d52 100644 --- a/node/src/reactor/main_reactor/error.rs +++ b/node/src/reactor/main_reactor/error.rs @@ -66,6 +66,10 @@ pub(crate) enum Error { /// `BinaryPort` component error. #[error("binary port: {0}")] BinaryPort(#[from] BinaryPortInitializationError), + + /// Failed to commit a protocol upgrade. + #[error("failed to commit protocol upgrade: {0}")] + ProtocolUpgrade(String), } impl From for Error { diff --git a/node/src/reactor/main_reactor/protocol_upgrade.rs b/node/src/reactor/main_reactor/protocol_upgrade.rs new file mode 100644 index 0000000000..5dad94a76e --- /dev/null +++ b/node/src/reactor/main_reactor/protocol_upgrade.rs @@ -0,0 +1,136 @@ +use std::{sync::Arc, time::Duration}; +use tokio::runtime::Handle; +use tracing::info; + +use casper_types::{ + BlockHash, BlockHeader, Chainspec, ChainspecRawBytes, Digest, EraId, TimeDiff, Timestamp, +}; +use datasize::DataSize; + +use crate::{components::contract_runtime::ContractRuntime, reactor::main_reactor::Error}; + +/// The information needed to produce the deterministic "immediate switch block" following a +/// protocol upgrade, once the node is ready to sign and gossip it. +#[derive(Clone, DataSize, Debug)] +pub(super) struct PendingImmediateSwitchBlock { + next_block_height: u64, + #[data_size(skip)] + post_state_hash: Digest, + parent_hash: BlockHash, + parent_seed: Digest, + era_id: EraId, + timestamp: Timestamp, +} + +impl PendingImmediateSwitchBlock { + pub(super) fn next_block_height(&self) -> u64 { + self.next_block_height + } + + pub(super) fn post_state_hash(&self) -> Digest { + self.post_state_hash + } + + pub(super) fn parent_hash(&self) -> BlockHash { + self.parent_hash + } + + pub(super) fn parent_seed(&self) -> Digest { + self.parent_seed + } + + pub(super) fn era_id(&self) -> EraId { + self.era_id + } + + pub(super) fn timestamp(&self) -> Timestamp { + self.timestamp + } +} + +/// If `tip_header` is a switch block that is the last block before the chainspec's +/// activation point, synchronously commits the protocol upgrade against `contract_runtime`'s +/// global state. Returns the info needed to later produce, sign, and gossip the resulting +/// immediate switch block, once the reactor is ready to do so (see +/// [`Self::maybe_finish_pending_upgrade`]). Returns `Ok(None)` if no upgrade is due +/// +/// This is an associated function (rather than a `&self` method) so it can be called from +/// `MainReactor::new`, before the reactor itself has been constructed -- that's the only +/// call site: a fresh restart whose local tip already sits at the pre-activation switch +/// block (e.g. after a live node shuts itself down for the upgrade). A node still *catching +/// up* through a historical activation point does not go through here; it just receives the +/// post-upgrade chain via the ordinary block-synchronizer fetch path, like any other +/// historical data. +pub(super) fn commit_upgrade_if_needed( + contract_runtime: &ContractRuntime, + chainspec: &Arc, + chainspec_raw_bytes: &Arc, + tip_header: Option<&BlockHeader>, + upgrade_timeout: TimeDiff, +) -> Result, Error> { + let Some(tip_header) = tip_header else { + return Ok(None); + }; + if !(tip_header.is_switch_block() + && tip_header.is_last_block_before_activation(&chainspec.protocol_config)) + { + return Ok(None); + } + + info!( + era_id = %tip_header.era_id(), + height = tip_header.height(), + "committing protocol upgrade" + ); + + let upgrade_config = chainspec + .upgrade_config_from_parts( + *tip_header.state_root_hash(), + tip_header.protocol_version(), + chainspec.protocol_config.activation_point.era_id(), + chainspec_raw_bytes.clone(), + ) + .map_err(Error::ProtocolUpgrade)?; + + // Executing protocol upgrade can be time consuming. It's executed in the background so the + // upgrade_timeout can be enforced. This function stays synchronous -- it's called + // from `MainReactor::new`, before the reactor's async event loop exists -- so the + // wait for that bounded future to resolve is bridged onto a dedicated scoped + // thread, which calls `Handle::block_on` directly. + let handle = Handle::current(); + let post_state_hash = std::thread::scope(|scope| { + scope + .spawn(|| { + handle.block_on(async { + match tokio::time::timeout( + Duration::from(upgrade_timeout), + contract_runtime.commit_protocol_upgrade(upgrade_config), + ) + .await + { + Ok(result) => result, + Err(_) => Err(format!( + "protocol upgrade did not complete within {}", + upgrade_timeout + )), + } + }) + }) + .join() + .unwrap_or_else(|panic| std::panic::resume_unwind(panic)) + }) + .map_err(Error::ProtocolUpgrade)?; + + Ok(Some(PendingImmediateSwitchBlock { + next_block_height: tip_header.height() + 1, + post_state_hash, + parent_hash: tip_header.block_hash(), + parent_seed: *tip_header.accumulated_seed(), + era_id: tip_header.next_block_era_id(), + // Adding one second here to make sure the timestamp is monotonically growing - + // it's important for EVM smart contracts + timestamp: tip_header + .timestamp() + .saturating_add(TimeDiff::from_seconds(1)), + })) +} diff --git a/node/src/reactor/main_reactor/reactor_state.rs b/node/src/reactor/main_reactor/reactor_state.rs index 5cbdff2011..6ea1c22d12 100644 --- a/node/src/reactor/main_reactor/reactor_state.rs +++ b/node/src/reactor/main_reactor/reactor_state.rs @@ -21,9 +21,7 @@ use serde::{Deserialize, Serialize}; /// CatchUp --> ShutdownAfterCatchingUp /// KeepUp --> ShutdownForUpgrade /// Validate --> ShutdownForUpgrade -/// CatchUp --> Upgrading /// CatchUp -->|at genesis| Validate -/// Upgrading --> CatchUp /// ShutdownForUpgrade --> End /// ``` /// ```mermaid @@ -63,8 +61,6 @@ pub enum ReactorState { Initialize, /// Orient to the network and attempt to catch up to tip. CatchUp, - /// Running commit upgrade and creating immediate switch block. - Upgrading, /// Stay caught up with tip. KeepUp, /// Node is currently caught up and is an active validator. diff --git a/node/src/reactor/main_reactor/tests.rs b/node/src/reactor/main_reactor/tests.rs index c9944e25dc..f7b9298199 100644 --- a/node/src/reactor/main_reactor/tests.rs +++ b/node/src/reactor/main_reactor/tests.rs @@ -2,9 +2,11 @@ mod auction; mod binary_port; mod configs_override; mod consensus_rules; +mod emergency_upgrade; mod fixture; mod gas_price; mod initial_stakes; +mod legacy_storage_reindex_and_upgrade; mod network_general; mod rejoining_node; mod rewards; diff --git a/node/src/reactor/main_reactor/tests/emergency_upgrade.rs b/node/src/reactor/main_reactor/tests/emergency_upgrade.rs new file mode 100644 index 0000000000..347dff2469 --- /dev/null +++ b/node/src/reactor/main_reactor/tests/emergency_upgrade.rs @@ -0,0 +1,105 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use casper_types::{ + ActivationPoint, ChainspecRawBytes, GlobalStateUpdate, ProtocolVersion, PublicKey, U512, +}; + +use crate::reactor::main_reactor::tests::{ + fixture::TestFixture, initial_stakes::InitialStakes, ERA_ONE, ERA_THREE, ERA_TWO, ONE_MIN, +}; + +/// Exercises an protocol upgrade that requires "peeling" blocks already stored under the old +/// protocol version. +/// +/// This also verifies that the resulting immediate switch block still gets signed and enough +/// finality signatures gossiped around the (freshly restarted) network to be marked complete, +/// even though the protocol upgrade itself is committed before the reactor has any peers. +#[tokio::test] +async fn emergency_upgrade_requiring_block_peeling() { + let initial_stakes = InitialStakes::AllEqual { + count: 4, + stake: 100_000_000_000, + }; + let mut fixture = TestFixture::new(initial_stakes, None).await; + + // Run the network well past era 2, as if the chain had kept producing blocks after the point + // an emergency upgrade should have activated. + fixture.run_until_consensus_in_era(ERA_THREE, ONE_MIN).await; + + // Build the "new" chainspec: a bumped protocol version, activating at era 2 (rolling back to + // the era 1 switch block -- everything from era 2 onward, produced under the old protocol + // version, must be peeled), with `hard_reset` set so storage actually performs that peel on + // restart, plus an emergency `global_state_update` re-confirming the validator set. + let mut new_chainspec = (*fixture.chainspec).clone(); + let old_version = new_chainspec.protocol_config.version; + let old_version_parts = old_version.value(); + let new_version = ProtocolVersion::from_parts( + old_version_parts.major, + old_version_parts.minor, + old_version_parts.patch + 1, + ); + new_chainspec.protocol_config.version = new_version; + new_chainspec.protocol_config.activation_point = ActivationPoint::EraId(ERA_TWO); + new_chainspec.protocol_config.hard_reset = true; + + let validators: BTreeMap<_, _> = fixture + .node_contexts + .iter() + .map(|node_context| { + ( + PublicKey::from(node_context.secret_key.as_ref()), + U512::from(100_000_000_000u64), + ) + }) + .collect(); + new_chainspec.protocol_config.global_state_update = Some(GlobalStateUpdate { + validators: Some(validators), + entries: BTreeMap::new(), + }); + let new_chainspec = Arc::new(new_chainspec); + let new_chainspec_raw_bytes: Arc = Arc::clone(&fixture.chainspec_raw_bytes); + + // Restart every node with the new chainspec, reusing its storage directory -- so the + // already-stored, now-stale, era-2+ blocks are still on disk to be peeled. + let node_count = fixture.node_contexts.len(); + let node_contexts: Vec<_> = (0..node_count) + .map(|_| fixture.remove_and_stop_node(0)) + .collect(); + for node_context in node_contexts { + fixture + .add_node_with_chainspec( + node_context.secret_key, + node_context.config, + node_context.storage_dir, + Arc::clone(&new_chainspec), + Arc::clone(&new_chainspec_raw_bytes), + ) + .await; + } + + // The network should come back up, apply the upgrade, and continue producing + // blocks + fixture.run_until_block_height(3, ONE_MIN).await; + + for runner in fixture.network.nodes().values() { + let storage = runner.main_reactor().storage(); + + // The era 1 switch block (height 2) predates the hard-reset era and must be untouched. + let era_one_switch_header = storage + .read_block_header_by_height(2, false) + .expect("should not error reading storage") + .expect("era 1 switch block should still be present"); + assert_eq!(era_one_switch_header.era_id(), ERA_ONE); + assert_eq!(era_one_switch_header.protocol_version(), old_version); + + // Any era-2+ blocks stored before the restart, under the OLD protocol version, must have + // been peeled: the immediate switch block at height 3 must be the first block of era 2, + // carrying the NEW protocol version. + let post_upgrade_header = storage + .read_block_header_by_height(3, false) + .expect("should not error reading storage") + .expect("post-upgrade immediate switch block should be present"); + assert_eq!(post_upgrade_header.era_id(), ERA_TWO); + assert_eq!(post_upgrade_header.protocol_version(), new_version); + } +} diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 106aaa5cee..dbebfc4fbb 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -122,6 +122,31 @@ impl TestFixture { stakes: BTreeMap, spec_override: Option, ) -> Self { + Self::new_with_keys_and_storage_dirs(rng, secret_keys, stakes, spec_override, None).await + } + + /// As [`Self::new_with_keys`], but if `existing_storage_dirs` is given, each node's storage + /// is pointed at the corresponding entry (by index, matching `secret_keys`) instead of a + /// fresh, empty temp dir -- used to boot the network from a pre-existing block store, e.g. + /// one produced by a different build of the node, to exercise on-disk backward-compatibility + /// behavior (such as rebuilding indexes that an older node version never persisted). + /// + /// As with [`Self::new`], runs the network until all nodes leave `ReactorState::Initialize`; + /// when resuming from non-empty storage this does not re-run genesis. + pub(crate) async fn new_with_keys_and_storage_dirs( + rng: TestRng, + secret_keys: Vec>, + stakes: BTreeMap, + spec_override: Option, + existing_storage_dirs: Option>>, + ) -> Self { + if let Some(dirs) = &existing_storage_dirs { + assert_eq!( + dirs.len(), + secret_keys.len(), + "existing_storage_dirs must have one entry per secret key" + ); + } testing::init_logging(); // Load the `local` chainspec. @@ -250,12 +275,16 @@ impl TestFixture { chainspec_raw_bytes: Arc::new(chainspec_raw_bytes), }; - for secret_key in secret_keys { - let (config, storage_dir) = fixture.create_node_config( + for (idx, secret_key) in secret_keys.into_iter().enumerate() { + let existing_storage_dir = existing_storage_dirs + .as_ref() + .map(|dirs| Arc::clone(&dirs[idx])); + let (config, storage_dir) = fixture.create_node_config_with_storage_dir( secret_key.as_ref(), None, storage_multiplier, node_config_override.clone(), + existing_storage_dir, ); fixture.add_node(secret_key, config, storage_dir).await; } @@ -382,6 +411,28 @@ impl TestFixture { maybe_trusted_hash: Option, storage_multiplier: u8, node_config_override: NodeConfigOverride, + ) -> (Config, Arc) { + self.create_node_config_with_storage_dir( + secret_key, + maybe_trusted_hash, + storage_multiplier, + node_config_override, + None, + ) + } + + /// As [`Self::create_node_config`], but if `existing_storage_dir` is given, points the node's + /// storage config at that directory instead of allocating a fresh, empty one -- used to boot + /// a node from a pre-existing block store (e.g. one produced by a different build of the + /// node, to exercise on-disk migration/backward-compatibility behavior). + #[track_caller] + pub(crate) fn create_node_config_with_storage_dir( + &mut self, + secret_key: &SecretKey, + maybe_trusted_hash: Option, + storage_multiplier: u8, + node_config_override: NodeConfigOverride, + existing_storage_dir: Option>, ) -> (Config, Arc) { // Set the network configuration. let network_cfg = match self.node_contexts.first() { @@ -417,8 +468,25 @@ impl TestFixture { cfg.node.idle_tolerance = idle } - // Additionally set up storage in a temporary directory. - let (storage_cfg, temp_dir) = storage::Config::new_for_tests(storage_multiplier); + // Additionally set up storage, either in a fresh temporary directory or, if given, an + // existing one already populated with a block store to resume from. + let (storage_cfg, temp_dir) = match existing_storage_dir { + Some(temp_dir) => { + let storage_cfg = storage::Config { + path: temp_dir.path().join("lmdb"), + max_block_store_size: 1024 * 1024 * storage_multiplier as usize, + max_deploy_store_size: 1024 * 1024 * storage_multiplier as usize, + max_deploy_metadata_store_size: 1024 * 1024 * storage_multiplier as usize, + max_state_store_size: 12 * 1024 * storage_multiplier as usize, + ..Default::default() + }; + (storage_cfg, temp_dir) + } + None => { + let (storage_cfg, temp_dir) = storage::Config::new_for_tests(storage_multiplier); + (storage_cfg, Arc::new(temp_dir)) + } + }; // ...and the secret key for our validator. { let secret_key_path = temp_dir.path().join("secret_key"); @@ -432,7 +500,7 @@ impl TestFixture { cfg.contract_runtime.max_global_state_size = Some(1024 * 1024 * storage_multiplier as usize); - (cfg, Arc::new(temp_dir)) + (cfg, temp_dir) } /// Adds a node to the network. @@ -467,6 +535,42 @@ impl TestFixture { id } + /// Adds a node to the network, running under the given chainspec rather than the fixture's + /// own -- used to simulate a node restarting with a new binary/chainspec version, e.g. after + /// a protocol upgrade. + /// + /// As with [`Self::add_node`], if re-adding a previously-removed node, the `secret_key`, + /// `config` and `storage_dir` returned in the `NodeContext` during removal should be used + /// here so the same storage dir is reused across both executions. + pub(crate) async fn add_node_with_chainspec( + &mut self, + secret_key: Arc, + config: Config, + storage_dir: Arc, + chainspec: Arc, + chainspec_raw_bytes: Arc, + ) -> NodeId { + let (id, _) = self + .network + .add_node_with_config_and_chainspec( + WithDir::new(RESOURCES_PATH.join("local"), config.clone()), + chainspec, + chainspec_raw_bytes, + &mut self.rng, + ) + .await + .expect("could not add node to reactor"); + let node_context = NodeContext { + id, + secret_key, + config, + storage_dir, + }; + self.node_contexts.push(node_context); + info!("added node {} with id {}", self.node_contexts.len() - 1, id); + id + } + pub(crate) async fn add_node_from_context_idx(&mut self, idx: usize) -> NodeId { let node_context = self.node_contexts.get(idx).unwrap(); let secret_key = node_context.secret_key.clone(); @@ -684,12 +788,29 @@ impl TestFixture { } pub(crate) async fn schedule_upgrade_for_era_two(&mut self) { + self.schedule_upgrade(ERA_TWO, ProtocolVersion::from_parts(999, 0, 0)) + .await; + } + + /// Announces an upcoming upgrade to every node's upgrade watcher, without touching its + /// chainspec -- as a real node would learn of one by picking up a new chainspec file dropped + /// alongside its current binary, ahead of the new binary itself being deployed. Each node + /// keeps running under its current protocol version until it reaches the switch block just + /// before `activation_era`, at which point it transitions to `ReactorState::ShutdownForUpgrade` + /// (see [`Self::run_until`] with a `ShutdownForUpgrade` condition, then restart the nodes with + /// a chainspec whose `activation_point` is `activation_era`, e.g. via + /// [`Self::add_node_with_chainspec`]). + pub(crate) async fn schedule_upgrade( + &mut self, + activation_era: EraId, + new_protocol_version: ProtocolVersion, + ) { for runner in self.network.runners_mut() { runner .process_injected_effects(|effect_builder| { let upgrade = NextUpgrade::new( - ActivationPoint::EraId(ERA_TWO), - ProtocolVersion::from_parts(999, 0, 0), + ActivationPoint::EraId(activation_era), + new_protocol_version, ); effect_builder .upgrade_watcher_announcement(Some(upgrade)) diff --git a/node/src/reactor/main_reactor/tests/legacy_storage_reindex_and_upgrade.rs b/node/src/reactor/main_reactor/tests/legacy_storage_reindex_and_upgrade.rs new file mode 100644 index 0000000000..ab37ca8c28 --- /dev/null +++ b/node/src/reactor/main_reactor/tests/legacy_storage_reindex_and_upgrade.rs @@ -0,0 +1,279 @@ +use std::{process::Command, sync::Arc}; + +use casper_types::{ + ActivationPoint, BlockV2, ChainspecRawBytes, EraId, PricingMode, ProtocolVersion, PublicKey, + SecretKey, Transaction, U512, +}; + +use crate::{ + reactor::main_reactor::{ + tests::{fixture::TestFixture, Nodes, ONE_MIN}, + ReactorState, + }, + types::transaction::transaction_v1_builder::TransactionV1Builder, +}; + +/// Path to a block store produced by an older node build that never persisted the +/// `block_height_index` / `switch_block_era_id_index` / `transaction_hash_index` databases (they +/// were kept purely in memory, rebuilt via a full header scan on every startup). Real blocks and +/// transactions across 3 eras, for 4 validators; captured once from `dev` and checked in as a +/// fixture -- see the module doc on [`boots_from_legacy_storage_reindexes_and_survives_upgrade`]. +/// +/// A single copy is committed, not one per node: the four validators that originally produced +/// this chain agree on identical blocks/transactions/global state by construction (that's what +/// consensus means), so any one of their block stores is a valid, fully interchangeable starting +/// point for any node in this test. (Their `storage.lmdb` files aren't literally byte-identical -- +/// LMDB's on-disk page layout isn't deterministic even for equivalent logical content -- but +/// `data.lmdb`, the global state, was confirmed byte-identical across all four when this fixture +/// was captured.) Per-validator consensus vote-history (`unit_files`) is deliberately not +/// included: it's specific to a validator's own identity, so reusing one node's copy for another +/// would be wrong, and going without it is fine here since era_supervisor just creates the +/// directory fresh and starts with no prior voting history, same as any other new process. +const LEGACY_FIXTURE_DIR: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/reactor/main_reactor/tests/resources/legacy_storage_no_index" +); + +fn fixed_secret_key(byte: u8) -> Arc { + Arc::new(SecretKey::ed25519_from_bytes([byte; SecretKey::ED25519_LENGTH]).unwrap()) +} + +fn transfer(from: &SecretKey, to: PublicKey, transfer_id: u64, chain_name: &str) -> Transaction { + let mut txn = Transaction::from( + TransactionV1Builder::new_transfer(30_000_000_000u64, None, to, Some(transfer_id)) + .unwrap() + .with_initiator_addr(PublicKey::from(from)) + .with_pricing_mode(PricingMode::Fixed { + gas_price_tolerance: 5, + additional_computation_factor: 0, + }) + .with_chain_name(chain_name.to_string()) + .build() + .unwrap(), + ); + txn.sign(from); + txn +} + +/// Boots a 4-validator network directly from a block store that predates the on-disk +/// `block_height_index` / `switch_block_era_id_index` / `transaction_hash_index` databases (real +/// blocks and transactions spanning 3 eras, produced by an older node build -- see +/// `LEGACY_FIXTURE_DIR`), and checks that: +/// +/// 1. The reactor detects the missing indexes and rebuilds them from a header scan on startup +/// (rather than erroring, or silently behaving as if the chain were empty) -- exercised here by +/// reading a switch block for an era predating the fixture, and by looking up the execution +/// results of transactions baked into the fixture, both of which are served via the rebuilt +/// indexes. +/// 2. The resumed network is fully live: it accepts and executes a brand new transaction. +/// 3. It survives a protocol upgrade: restarting with a bumped protocol version and a future +/// activation point, the network continues past the activation era under the new protocol +/// version, produces the resulting immediate switch block, and keeps processing transactions +/// afterwards. +#[tokio::test] +async fn boots_from_legacy_storage_reindexes_and_survives_upgrade() { + let secret_keys: Vec> = (1..=4u8).map(fixed_secret_key).collect(); + let public_keys: Vec = secret_keys + .iter() + .map(|key| PublicKey::from(key.as_ref())) + .collect(); + let stakes = public_keys + .iter() + .cloned() + .map(|public_key| { + ( + public_key, + ( + U512::from(700_000_000_000_000_000u64), + U512::from(100_000_000_000_000u64), + ), + ) + }) + .collect(); + + // Copy the single committed fixture into a fresh temp dir per node (so the test doesn't + // mutate checked-in data, and each node gets its own independent, isolated copy to run + // against -- see the doc on `LEGACY_FIXTURE_DIR` for why one copy is enough). + let mut storage_dirs = Vec::new(); + for _ in 0..4 { + let temp_dir = tempfile::tempdir().expect("should create temp dir"); + let status = Command::new("cp") + .arg("-r") + .arg(format!("{LEGACY_FIXTURE_DIR}/.")) + .arg(temp_dir.path()) + .status() + .expect("failed to spawn cp"); + assert!(status.success(), "cp failed"); + storage_dirs.push(Arc::new(temp_dir)); + } + + let rng = casper_types::testing::TestRng::new(); + let mut fixture = TestFixture::new_with_keys_and_storage_dirs( + rng, + secret_keys.clone(), + stakes, + None, + Some(storage_dirs), + ) + .await; + + // The fixture was captured at height 6, era 3 -- confirm we resumed it rather than starting a + // fresh genesis (which would be height 0, era 0). + let resumed_at = fixture.highest_complete_block(); + assert!( + resumed_at.height() >= 6, + "should have resumed the pre-existing chain from the legacy fixture, not restarted genesis" + ); + + // The switch-block-era-id index was empty on disk; this only succeeds if it was rebuilt. + let _ = fixture.switch_block(EraId::new(1)); + + // The transaction-hash index was empty on disk too; look up the execution results of the + // transactions actually baked into the fixture's blocks (rather than recomputing their + // hashes, which aren't reproducible -- the builder stamps each with `Timestamp::now()`). + let node_0 = fixture.node_contexts[0].id; + let mut legacy_txn_count = 0; + for height in 1..=resumed_at.height() { + let Ok(block_v2) = BlockV2::try_from(fixture.get_block_by_height(height)) else { + continue; + }; + for txn_hash in block_v2.all_transactions() { + let result = fixture + .network + .nodes() + .get(&node_0) + .expect("should have node 0") + .main_reactor() + .storage() + .read_execution_result(txn_hash); + assert!( + result.is_some(), + "transaction {txn_hash} from the legacy fixture should be readable via the \ + rebuilt transaction_hash_index" + ); + legacy_txn_count += 1; + } + } + assert_eq!( + legacy_txn_count, 3, + "expected to find the 3 transfers baked into the legacy fixture" + ); + + // The resumed network should be fully live: it can accept and execute a new transaction. + let chain_name = fixture.chainspec.network_config.name.clone(); + let pre_upgrade_txn = transfer( + secret_keys[0].as_ref(), + public_keys[1].clone(), + 100, + &chain_name, + ); + let pre_upgrade_txn_hash = pre_upgrade_txn.hash(); + fixture.inject_transaction(pre_upgrade_txn).await; + fixture + .run_until_executed_transaction(&pre_upgrade_txn_hash, ONE_MIN) + .await; + + // Now drive it through a protocol upgrade, the same two-phase way a real deployment would: + // + // 1. Announce the upcoming upgrade (a new chainspec dropped next to the still-running old + // binary) via the upgrade watcher, without touching the running chainspec. The network keeps + // validating under the old protocol version until it reaches the switch block just before + // the activation era, at which point every node shuts down for upgrade. + // 2. Only then restart every node -- reusing its (now-fully-indexed) storage dir -- with a + // chainspec whose `activation_point` is that same era. `activation_point` must never be a + // not-yet-reached era relative to a *live* chainspec: `ChainspecConsensusExt` treats it as + // "the era immediately after the most recent upgrade or restart", so setting it to a future + // era on a running chainspec (rather than one being restarted right at that point) trips + // `earliest_relevant_era`'s invariant in `EraSupervisor::create_required_eras`. + // + // No hard reset / global state update: this is an ordinary forward upgrade, not an emergency + // rollback (contrast with `emergency_upgrade.rs`, which upgrades at an already-passed era). + let activation_era = fixture + .highest_complete_block() + .era_id() + .successor() + .successor(); + let old_version = fixture.chainspec.protocol_config.version; + let old_version_parts = old_version.value(); + let new_version = ProtocolVersion::from_parts( + old_version_parts.major, + old_version_parts.minor, + old_version_parts.patch + 1, + ); + + fixture.schedule_upgrade(activation_era, new_version).await; + // Wait not just for every node to report `ShutdownForUpgrade`, but for their local tips to + // have actually converged on the same block first: a node can flip its reactor state to + // `ShutdownForUpgrade` while a peer is still finishing executing/storing the last block or + // two under the old protocol version, and stopping nodes non-atomically while they're still + // staggered like that risks storing conflicting blocks at the same height across them. + fixture + .run_until( + |nodes: &Nodes| { + if !nodes + .values() + .all(|runner| runner.main_reactor().state == ReactorState::ShutdownForUpgrade) + { + return false; + } + let mut tip_hashes = nodes.values().map(|runner| { + runner + .main_reactor() + .storage() + .get_highest_complete_block() + .ok() + .flatten() + .map(|block| *block.hash()) + }); + let Some(first) = tip_hashes.next() else { + return false; + }; + tip_hashes.all(|hash| hash == first) + }, + ONE_MIN, + ) + .await; + + let mut new_chainspec = (*fixture.chainspec).clone(); + new_chainspec.protocol_config.version = new_version; + new_chainspec.protocol_config.activation_point = ActivationPoint::EraId(activation_era); + let new_chainspec = Arc::new(new_chainspec); + let new_chainspec_raw_bytes: Arc = Arc::clone(&fixture.chainspec_raw_bytes); + + let node_count = fixture.node_contexts.len(); + let node_contexts: Vec<_> = (0..node_count) + .map(|_| fixture.remove_and_stop_node(0)) + .collect(); + for node_context in node_contexts { + fixture + .add_node_with_chainspec( + node_context.secret_key, + node_context.config, + node_context.storage_dir, + Arc::clone(&new_chainspec), + Arc::clone(&new_chainspec_raw_bytes), + ) + .await; + } + + // The network should come back up, commit the upgrade, and produce the resulting immediate + // switch block under the new protocol version. + fixture + .run_until_stored_switch_block_header(activation_era, ONE_MIN) + .await; + let post_upgrade_header = fixture.switch_block(activation_era); + assert_eq!(post_upgrade_header.protocol_version(), new_version); + + // And the upgraded network is still fully live. + let post_upgrade_txn = transfer( + secret_keys[1].as_ref(), + public_keys[2].clone(), + 101, + &chain_name, + ); + let post_upgrade_txn_hash = post_upgrade_txn.hash(); + fixture.inject_transaction(post_upgrade_txn).await; + fixture + .run_until_executed_transaction(&post_upgrade_txn_hash, ONE_MIN) + .await; +} diff --git a/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/data.lmdb b/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/data.lmdb new file mode 100644 index 0000000000..ff13f3c949 Binary files /dev/null and b/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/data.lmdb differ diff --git a/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/storage.lmdb b/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/storage.lmdb new file mode 100644 index 0000000000..6ce199db52 Binary files /dev/null and b/node/src/reactor/main_reactor/tests/resources/legacy_storage_no_index/lmdb/casper-example/storage.lmdb differ diff --git a/node/src/reactor/main_reactor/upgrading_instruction.rs b/node/src/reactor/main_reactor/upgrading_instruction.rs deleted file mode 100644 index 63e44af633..0000000000 --- a/node/src/reactor/main_reactor/upgrading_instruction.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::time::Duration; - -use casper_types::{TimeDiff, Timestamp}; - -pub(super) enum UpgradingInstruction { - CheckLater(String, Duration), - CatchUp, -} - -impl UpgradingInstruction { - pub(super) fn should_commit_upgrade( - should_commit_upgrade: bool, - wait: Duration, - last_progress: Timestamp, - upgrade_timeout: TimeDiff, - ) -> UpgradingInstruction { - if should_commit_upgrade { - if last_progress.elapsed() > upgrade_timeout { - UpgradingInstruction::CatchUp - } else { - UpgradingInstruction::CheckLater("awaiting upgrade".to_string(), wait) - } - } else { - UpgradingInstruction::CatchUp - } - } -} diff --git a/node/src/types/node_config.rs b/node/src/types/node_config.rs index f0f2081a3d..bb04cfed6a 100644 --- a/node/src/types/node_config.rs +++ b/node/src/types/node_config.rs @@ -91,6 +91,11 @@ pub struct NodeConfig { /// If true, prevents a node from shutting down if it is supposed to be a validator in the era. pub prevent_validator_shutdown: bool, + + /// If true, skips committing a protocol upgrade locally when the node's tip is the last + /// block before the activation point, and instead lets the node acquire the post-upgrade + /// chain via the ordinary block-synchronizer fetch path, as if it were catching up. + pub skip_protocol_upgrade: bool, } impl Default for NodeConfig { @@ -105,6 +110,7 @@ impl Default for NodeConfig { shutdown_for_upgrade_timeout: DEFAULT_SHUTDOWN_FOR_UPGRADE_TIMEOUT.parse().unwrap(), upgrade_timeout: DEFAULT_UPGRADE_TIMEOUT.parse().unwrap(), prevent_validator_shutdown: false, + skip_protocol_upgrade: false, } } } diff --git a/resources/integration-test/config-example.toml b/resources/integration-test/config-example.toml index 72a782ef34..2e14f11abc 100644 --- a/resources/integration-test/config-example.toml +++ b/resources/integration-test/config-example.toml @@ -68,6 +68,11 @@ upgrade_timeout = '30 seconds' # other restarting nodes. This config is inert on non-validating nodes. prevent_validator_shutdown = false +# If true, skips committing a protocol upgrade locally when this node's tip is the last +# block before the upgrade's activation point. Instead, the node will acquire the +# post-upgrade chain via the ordinary syncing process, as if it were catching up. +skip_protocol_upgrade = false + # ================================= # Configuration options for logging # ================================= diff --git a/resources/local/config.toml b/resources/local/config.toml index d7692ec4ed..17cfd20872 100644 --- a/resources/local/config.toml +++ b/resources/local/config.toml @@ -68,6 +68,11 @@ upgrade_timeout = '3 hours' # other restarting nodes. This config is inert on non-validating nodes. prevent_validator_shutdown = false +# If true, skips committing a protocol upgrade locally when this node's tip is the last +# block before the upgrade's activation point. Instead, the node will acquire the +# post-upgrade chain via the ordinary syncing process, as if it were catching up. +skip_protocol_upgrade = false + # ================================= # Configuration options for logging # ================================= diff --git a/resources/mainnet/config-example.toml b/resources/mainnet/config-example.toml index dba2fa6f17..3b60eec6f7 100644 --- a/resources/mainnet/config-example.toml +++ b/resources/mainnet/config-example.toml @@ -68,6 +68,11 @@ upgrade_timeout = '30 seconds' # other restarting nodes. This config is inert on non-validating nodes. prevent_validator_shutdown = false +# If true, skips committing a protocol upgrade locally when this node's tip is the last +# block before the upgrade's activation point. Instead, the node will acquire the +# post-upgrade chain via the ordinary syncing process, as if it were catching up. +skip_protocol_upgrade = false + # ================================= # Configuration options for logging # ================================= diff --git a/resources/production/config-example.toml b/resources/production/config-example.toml index 861697c3f7..f4579254b7 100644 --- a/resources/production/config-example.toml +++ b/resources/production/config-example.toml @@ -68,6 +68,11 @@ upgrade_timeout = '30 seconds' # other restarting nodes. This config is inert on non-validating nodes. prevent_validator_shutdown = false +# If true, skips committing a protocol upgrade locally when this node's tip is the last +# block before the upgrade's activation point. Instead, the node will acquire the +# post-upgrade chain via the ordinary syncing process, as if it were catching up. +skip_protocol_upgrade = false + # ================================= # Configuration options for logging # ================================= diff --git a/resources/test/rest_schema_status.json b/resources/test/rest_schema_status.json index f1a156d42d..29d816c307 100644 --- a/resources/test/rest_schema_status.json +++ b/resources/test/rest_schema_status.json @@ -308,13 +308,6 @@ "CatchUp" ] }, - { - "description": "Running commit upgrade and creating immediate switch block.", - "type": "string", - "enum": [ - "Upgrading" - ] - }, { "description": "Stay caught up with tip.", "type": "string", diff --git a/resources/testnet/config-example.toml b/resources/testnet/config-example.toml index b69e0900f8..aee450f57c 100644 --- a/resources/testnet/config-example.toml +++ b/resources/testnet/config-example.toml @@ -68,6 +68,11 @@ upgrade_timeout = '30 seconds' # other restarting nodes. This config is inert on non-validating nodes. prevent_validator_shutdown = false +# If true, skips committing a protocol upgrade locally when this node's tip is the last +# block before the upgrade's activation point. Instead, the node will acquire the +# post-upgrade chain via the ordinary syncing process, as if it were catching up. +skip_protocol_upgrade = false + # ================================= # Configuration options for logging # ================================= diff --git a/storage/Cargo.toml b/storage/Cargo.toml index 8e1de74053..a5e486f769 100644 --- a/storage/Cargo.toml +++ b/storage/Cargo.toml @@ -16,6 +16,7 @@ casper-types = { version = "7.0.0", path = "../types", features = ["datasize", " datasize = "0.2.4" either = "1.8.1" lmdb-rkv = "0.14" +lmdb-rkv-sys = "0.11" num = { version = "0.4.0", default-features = false } num-derive = { workspace = true } num-rational = { version = "0.4.0", features = ["serde"] } diff --git a/storage/src/block_store/lmdb/indexed_lmdb_block_store.rs b/storage/src/block_store/lmdb/indexed_lmdb_block_store.rs deleted file mode 100644 index 2db3030e60..0000000000 --- a/storage/src/block_store/lmdb/indexed_lmdb_block_store.rs +++ /dev/null @@ -1,1256 +0,0 @@ -use std::{ - borrow::Cow, - collections::{btree_map, hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}, -}; - -use super::{ - lmdb_block_store::LmdbBlockStore, lmdb_ext::LmdbExtError, temp_map::TempMap, DbTableId, -}; -use datasize::DataSize; -use lmdb::{ - Environment, RoTransaction, RwCursor, RwTransaction, Transaction as LmdbTransaction, WriteFlags, -}; - -use tracing::info; - -use super::versioned_databases::VersionedDatabases; -use crate::block_store::{ - block_provider::{BlockStoreTransaction, DataReader, DataWriter}, - types::{ - ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockHeight, BlockTransfers, - LatestSwitchBlock, StateStore, StateStoreKey, Tip, TransactionFinalizedApprovals, - }, - BlockStoreError, BlockStoreProvider, DbRawBytesSpec, -}; -use casper_types::{ - execution::ExecutionResult, Approval, Block, BlockBody, BlockHash, BlockHeader, - BlockSignatures, Digest, EraId, ProtocolVersion, Transaction, TransactionHash, Transfer, -}; - -/// Indexed lmdb block store. -#[derive(DataSize, Debug)] -pub struct IndexedLmdbBlockStore { - /// Block store - block_store: LmdbBlockStore, - /// A map of block height to block ID. - block_height_index: BTreeMap, - /// A map of era ID to switch block ID. - switch_block_era_id_index: BTreeMap, - /// A map of transaction hashes to hashes, heights and era IDs of blocks containing them. - transaction_hash_index: BTreeMap, -} - -impl IndexedLmdbBlockStore { - fn get_reader(&self) -> Result, BlockStoreError> { - let txn = self - .block_store - .env - .begin_ro_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - Ok(IndexedLmdbBlockStoreReadTransaction { - txn, - block_store: self, - }) - } - - /// Inserts the relevant entries to the index. - /// - /// If a duplicate entry is encountered, index is not updated and an error is returned. - fn insert_to_transaction_index( - transaction_hash_index: &mut BTreeMap, - block_hash: BlockHash, - block_height: u64, - era_id: EraId, - transaction_hashes: Vec, - ) -> Result<(), BlockStoreError> { - if let Some(hash) = transaction_hashes.iter().find(|hash| { - transaction_hash_index - .get(hash) - .is_some_and(|old_details| old_details.block_hash != block_hash) - }) { - return Err(BlockStoreError::DuplicateTransaction { - transaction_hash: *hash, - first: transaction_hash_index[hash].block_hash, - second: block_hash, - }); - } - - for hash in transaction_hashes { - transaction_hash_index.insert( - hash, - BlockHashHeightAndEra::new(block_hash, block_height, era_id), - ); - } - - Ok(()) - } - - /// Inserts the relevant entries to the two indices. - /// - /// If a duplicate entry is encountered, neither index is updated and an error is returned. - pub(super) fn insert_to_block_header_indices( - block_height_index: &mut BTreeMap, - switch_block_era_id_index: &mut BTreeMap, - block_header: &BlockHeader, - ) -> Result<(), BlockStoreError> { - let block_hash = block_header.block_hash(); - if let Some(first) = block_height_index.get(&block_header.height()) { - if *first != block_hash { - return Err(BlockStoreError::DuplicateBlock { - height: block_header.height(), - first: *first, - second: block_hash, - }); - } - } - - if block_header.is_switch_block() { - match switch_block_era_id_index.entry(block_header.era_id()) { - btree_map::Entry::Vacant(entry) => { - let _ = entry.insert(block_hash); - } - btree_map::Entry::Occupied(entry) => { - if *entry.get() != block_hash { - return Err(BlockStoreError::DuplicateEraId { - era_id: block_header.era_id(), - first: *entry.get(), - second: block_hash, - }); - } - } - } - } - - let _ = block_height_index.insert(block_header.height(), block_hash); - Ok(()) - } - - /// Ctor. - pub fn new( - block_store: LmdbBlockStore, - hard_reset_to_start_of_era: Option, - protocol_version: ProtocolVersion, - ) -> Result { - // We now need to restore the block-height index. Log messages allow timing here. - info!("indexing block store"); - let mut block_height_index = BTreeMap::new(); - let mut switch_block_era_id_index = BTreeMap::new(); - let mut transaction_hash_index = BTreeMap::new(); - let mut block_txn = block_store - .env - .begin_rw_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - - let mut deleted_block_hashes = HashSet::new(); - // Map of all block body hashes, with their values representing whether to retain the - // corresponding block bodies or not. - let mut block_body_hashes = HashMap::new(); - let mut deleted_transaction_hashes = HashSet::::new(); - - let mut init_fn = - |cursor: &mut RwCursor, block_header: BlockHeader| -> Result<(), BlockStoreError> { - let should_retain_block = match hard_reset_to_start_of_era { - Some(invalid_era) => { - // Retain blocks from eras before the hard reset era, and blocks after this - // era if they are from the current protocol version (as otherwise a node - // restart would purge them again, despite them being valid). - block_header.era_id() < invalid_era - || block_header.protocol_version() == protocol_version - } - None => true, - }; - - // If we don't already have the block body hash in the collection, insert it with - // the value `should_retain_block`. - // - // If there is an existing value, the updated value should be `false` iff the - // existing value and `should_retain_block` are both `false`. - // Otherwise the updated value should be `true`. - match block_body_hashes.entry(*block_header.body_hash()) { - Entry::Vacant(entry) => { - entry.insert(should_retain_block); - } - Entry::Occupied(entry) => { - let value = entry.into_mut(); - *value = *value || should_retain_block; - } - } - - let body_txn = block_store - .env - .begin_ro_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - let maybe_block_body = block_store - .block_body_dbs - .get(&body_txn, block_header.body_hash()) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - if !should_retain_block { - let _ = deleted_block_hashes.insert(block_header.block_hash()); - - match &maybe_block_body { - Some(BlockBody::V1(v1_body)) => deleted_transaction_hashes.extend( - v1_body - .deploy_and_transfer_hashes() - .map(TransactionHash::from), - ), - Some(BlockBody::V2(v2_body)) => { - let transactions = v2_body.all_transactions(); - deleted_transaction_hashes.extend(transactions) - } - None => (), - } - - cursor - .del(WriteFlags::empty()) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - return Ok(()); - } - - Self::insert_to_block_header_indices( - &mut block_height_index, - &mut switch_block_era_id_index, - &block_header, - )?; - - if let Some(block_body) = maybe_block_body { - let transaction_hashes = match block_body { - BlockBody::V1(v1) => v1 - .deploy_and_transfer_hashes() - .map(TransactionHash::from) - .collect(), - BlockBody::V2(v2) => v2.all_transactions().copied().collect(), - }; - Self::insert_to_transaction_index( - &mut transaction_hash_index, - block_header.block_hash(), - block_header.height(), - block_header.era_id(), - transaction_hashes, - )?; - } - - Ok(()) - }; - - block_store - .block_header_dbs - .for_each_value_in_current(&mut block_txn, &mut init_fn)?; - block_store - .block_header_dbs - .for_each_value_in_legacy(&mut block_txn, &mut init_fn)?; - - info!("block store reindexing complete"); - block_txn - .commit() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - - let deleted_block_body_hashes = block_body_hashes - .into_iter() - .filter_map(|(body_hash, retain)| (!retain).then_some(body_hash)) - .collect(); - initialize_block_body_dbs( - &block_store.env, - block_store.block_body_dbs, - deleted_block_body_hashes, - )?; - initialize_block_metadata_dbs( - &block_store.env, - block_store.block_metadata_dbs, - deleted_block_hashes, - )?; - initialize_execution_result_dbs( - &block_store.env, - block_store.execution_result_dbs, - deleted_transaction_hashes, - ) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - - Ok(Self { - block_store, - block_height_index, - switch_block_era_id_index, - transaction_hash_index, - }) - } -} - -/// Purges stale entries from the block body databases. -fn initialize_block_body_dbs( - env: &Environment, - block_body_dbs: VersionedDatabases, - deleted_block_body_hashes: HashSet, -) -> Result<(), BlockStoreError> { - info!("initializing block body databases"); - let mut txn = env - .begin_rw_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - for body_hash in deleted_block_body_hashes { - block_body_dbs - .delete(&mut txn, &body_hash) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - } - txn.commit() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - info!("block body database initialized"); - Ok(()) -} - -/// Purges stale entries from the block metadata database. -fn initialize_block_metadata_dbs( - env: &Environment, - block_metadata_dbs: VersionedDatabases, - deleted_block_hashes: HashSet, -) -> Result<(), BlockStoreError> { - let block_count_to_be_deleted = deleted_block_hashes.len(); - info!( - block_count_to_be_deleted, - "initializing block metadata database" - ); - let mut txn = env - .begin_rw_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - for block_hash in deleted_block_hashes { - block_metadata_dbs - .delete(&mut txn, &block_hash) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? - } - txn.commit() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - info!("block metadata database initialized"); - Ok(()) -} - -/// Purges stale entries from the execution result databases. -fn initialize_execution_result_dbs( - env: &Environment, - execution_result_dbs: VersionedDatabases, - deleted_transaction_hashes: HashSet, -) -> Result<(), LmdbExtError> { - let exec_results_count_to_be_deleted = deleted_transaction_hashes.len(); - info!( - exec_results_count_to_be_deleted, - "initializing execution result databases" - ); - let mut txn = env.begin_rw_txn()?; - for hash in deleted_transaction_hashes { - execution_result_dbs.delete(&mut txn, &hash)?; - } - txn.commit()?; - info!("execution result databases initialized"); - Ok(()) -} - -pub struct IndexedLmdbBlockStoreRWTransaction<'t> { - txn: RwTransaction<'t>, - block_store: &'t LmdbBlockStore, - block_height_index: TempMap<'t, u64, BlockHash>, - switch_block_era_id_index: TempMap<'t, EraId, BlockHash>, - transaction_hash_index: TempMap<'t, TransactionHash, BlockHashHeightAndEra>, -} - -impl IndexedLmdbBlockStoreRWTransaction<'_> { - /// Check if the block height index can be updated. - fn should_update_block_height_index( - &self, - block_height: u64, - block_hash: &BlockHash, - ) -> Result { - if let Some(first) = self.block_height_index.get(&block_height) { - // There is a block in the index at this height - if first != *block_hash { - Err(BlockStoreError::DuplicateBlock { - height: block_height, - first, - second: *block_hash, - }) - } else { - // Same value already in index, no need to update it. - Ok(false) - } - } else { - // Value not in index, update. - Ok(true) - } - } - - /// Check if the switch block index can be updated. - fn should_update_switch_block_index( - &self, - block_header: &BlockHeader, - ) -> Result { - if block_header.is_switch_block() { - let era_id = block_header.era_id(); - if let Some(entry) = self.switch_block_era_id_index.get(&era_id) { - let block_hash = block_header.block_hash(); - if entry != block_hash { - Err(BlockStoreError::DuplicateEraId { - era_id, - first: entry, - second: block_hash, - }) - } else { - // already in index, no need to update. - Ok(false) - } - } else { - // not in the index, update. - Ok(true) - } - } else { - // not a switch block. - Ok(false) - } - } - - // Check if the transaction hash index can be updated. - fn should_update_transaction_hash_index( - &self, - transaction_hashes: &[TransactionHash], - block_hash: &BlockHash, - ) -> Result { - if let Some(hash) = transaction_hashes.iter().find(|hash| { - self.transaction_hash_index - .get(hash) - .is_some_and(|old_details| old_details.block_hash != *block_hash) - }) { - return Err(BlockStoreError::DuplicateTransaction { - transaction_hash: *hash, - first: self.transaction_hash_index.get(hash).unwrap().block_hash, - second: *block_hash, - }); - } - Ok(true) - } -} - -pub struct IndexedLmdbBlockStoreReadTransaction<'t> { - txn: RoTransaction<'t>, - block_store: &'t IndexedLmdbBlockStore, -} - -enum LmdbBlockStoreIndex { - BlockHeight(IndexPosition), - SwitchBlockEraId(IndexPosition), -} - -enum IndexPosition { - Tip, - Key(K), -} - -enum DataType { - Block, - BlockHeader, - ApprovalsHashes, - BlockSignatures, -} - -impl IndexedLmdbBlockStoreReadTransaction<'_> { - fn block_hash_from_index(&self, index: LmdbBlockStoreIndex) -> Option<&BlockHash> { - match index { - LmdbBlockStoreIndex::BlockHeight(position) => match position { - IndexPosition::Tip => self.block_store.block_height_index.values().last(), - IndexPosition::Key(height) => self.block_store.block_height_index.get(&height), - }, - LmdbBlockStoreIndex::SwitchBlockEraId(position) => match position { - IndexPosition::Tip => self.block_store.switch_block_era_id_index.values().last(), - IndexPosition::Key(era_id) => { - self.block_store.switch_block_era_id_index.get(&era_id) - } - }, - } - } - - fn read_block_indexed( - &self, - index: LmdbBlockStoreIndex, - ) -> Result, BlockStoreError> { - self.block_hash_from_index(index) - .and_then(|block_hash| { - self.block_store - .block_store - .get_single_block(&self.txn, block_hash) - .transpose() - }) - .transpose() - } - - fn read_block_header_indexed( - &self, - index: LmdbBlockStoreIndex, - ) -> Result, BlockStoreError> { - self.block_hash_from_index(index) - .and_then(|block_hash| { - self.block_store - .block_store - .get_single_block_header(&self.txn, block_hash) - .transpose() - }) - .transpose() - } - - fn read_block_signatures_indexed( - &self, - index: LmdbBlockStoreIndex, - ) -> Result, BlockStoreError> { - self.block_hash_from_index(index) - .and_then(|block_hash| { - self.block_store - .block_store - .get_block_signatures(&self.txn, block_hash) - .transpose() - }) - .transpose() - } - - fn read_approvals_hashes_indexed( - &self, - index: LmdbBlockStoreIndex, - ) -> Result, BlockStoreError> { - self.block_hash_from_index(index) - .and_then(|block_hash| { - self.block_store - .block_store - .read_approvals_hashes(&self.txn, block_hash) - .transpose() - }) - .transpose() - } - - fn contains_data_indexed( - &self, - index: LmdbBlockStoreIndex, - data_type: DataType, - ) -> Result { - self.block_hash_from_index(index) - .map_or(Ok(false), |block_hash| match data_type { - DataType::Block => self - .block_store - .block_store - .block_exists(&self.txn, block_hash), - DataType::BlockHeader => self - .block_store - .block_store - .block_header_exists(&self.txn, block_hash), - DataType::ApprovalsHashes => self - .block_store - .block_store - .approvals_hashes_exist(&self.txn, block_hash), - DataType::BlockSignatures => self - .block_store - .block_store - .block_signatures_exist(&self.txn, block_hash), - }) - } - - pub fn get_switch_block_height(&self, era_id: EraId) -> Result, BlockStoreError> { - let index = LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(era_id)); - match self.block_hash_from_index(index) { - Some(block_hash) => { - let maybe_header: Option = self.read(*block_hash)?; - Ok(maybe_header.map(|header| header.height())) - } - None => Ok(None), - } - } -} - -impl BlockStoreTransaction for IndexedLmdbBlockStoreReadTransaction<'_> { - fn commit(self) -> Result<(), BlockStoreError> { - Ok(()) - } - - fn rollback(self) { - self.txn.abort(); - } -} - -impl BlockStoreTransaction for IndexedLmdbBlockStoreRWTransaction<'_> { - fn commit(self) -> Result<(), BlockStoreError> { - self.txn - .commit() - .map_err(|e| BlockStoreError::InternalStorage(Box::new(LmdbExtError::from(e))))?; - - self.block_height_index.commit(); - self.switch_block_era_id_index.commit(); - self.transaction_hash_index.commit(); - Ok(()) - } - - fn rollback(self) { - self.txn.abort(); - } -} - -impl BlockStoreProvider for IndexedLmdbBlockStore { - type Reader<'t> = IndexedLmdbBlockStoreReadTransaction<'t>; - type ReaderWriter<'t> = IndexedLmdbBlockStoreRWTransaction<'t>; - - fn checkout_ro(&self) -> Result, BlockStoreError> { - self.get_reader() - } - - fn checkout_rw(&mut self) -> Result, BlockStoreError> { - let txn = self - .block_store - .env - .begin_rw_txn() - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; - - Ok(IndexedLmdbBlockStoreRWTransaction { - txn, - block_store: &self.block_store, - block_height_index: TempMap::new(&mut self.block_height_index), - switch_block_era_id_index: TempMap::new(&mut self.switch_block_era_id_index), - transaction_hash_index: TempMap::new(&mut self.transaction_hash_index), - }) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .get_single_block(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store.block_store.block_exists(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .get_single_block_header(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store - .block_store - .block_header_exists(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .read_approvals_hashes(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store - .block_store - .block_header_exists(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .get_block_signatures(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store - .block_store - .block_signatures_exist(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { - self.read_block_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key))) - } - - fn exists(&self, key: BlockHeight) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), - DataType::Block, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { - self.read_block_header_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key))) - } - - fn exists(&self, key: BlockHeight) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), - DataType::BlockHeader, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { - self.read_approvals_hashes_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key( - key, - ))) - } - - fn exists(&self, key: BlockHeight) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), - DataType::ApprovalsHashes, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { - self.read_block_signatures_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key( - key, - ))) - } - - fn exists(&self, key: BlockHeight) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), - DataType::BlockSignatures, - ) - } -} - -/// Retrieves single switch block by era ID by looking it up in the index and returning it. -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: EraId) -> Result, BlockStoreError> { - self.read_block_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key( - key, - ))) - } - - fn exists(&self, key: EraId) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), - DataType::Block, - ) - } -} - -/// Retrieves single switch block header by era ID by looking it up in the index and returning -/// it. -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: EraId) -> Result, BlockStoreError> { - self.read_block_header_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key( - key, - ))) - } - - fn exists(&self, key: EraId) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), - DataType::BlockHeader, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: EraId) -> Result, BlockStoreError> { - self.read_approvals_hashes_indexed(LmdbBlockStoreIndex::SwitchBlockEraId( - IndexPosition::Key(key), - )) - } - - fn exists(&self, key: EraId) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), - DataType::ApprovalsHashes, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: EraId) -> Result, BlockStoreError> { - self.read_block_signatures_indexed(LmdbBlockStoreIndex::SwitchBlockEraId( - IndexPosition::Key(key), - )) - } - - fn exists(&self, key: EraId) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), - DataType::BlockSignatures, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, _key: Tip) -> Result, BlockStoreError> { - self.read_block_header_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip)) - } - - fn exists(&self, _key: Tip) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip), - DataType::BlockHeader, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, _key: Tip) -> Result, BlockStoreError> { - self.read_block_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip)) - } - - fn exists(&self, _key: Tip) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip), - DataType::Block, - ) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, _key: LatestSwitchBlock) -> Result, BlockStoreError> { - self.read_block_header_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Tip)) - } - - fn exists(&self, _key: LatestSwitchBlock) -> Result { - self.contains_data_indexed( - LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Tip), - DataType::BlockHeader, - ) - } -} - -impl DataReader - for IndexedLmdbBlockStoreReadTransaction<'_> -{ - fn read(&self, key: TransactionHash) -> Result, BlockStoreError> { - Ok(self.block_store.transaction_hash_index.get(&key).copied()) - } - - fn exists(&self, key: TransactionHash) -> Result { - Ok(self.block_store.transaction_hash_index.contains_key(&key)) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: TransactionHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .transaction_dbs - .get(&self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, key: TransactionHash) -> Result { - self.block_store - .block_store - .transaction_exists(&self.txn, &key) - } -} - -impl DataReader> for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: TransactionHash) -> Result>, BlockStoreError> { - self.block_store - .block_store - .finalized_transaction_approvals_dbs - .get(&self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, key: TransactionHash) -> Result { - self.block_store - .block_store - .finalized_transaction_approvals_dbs - .exists(&self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } -} - -impl DataReader for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, key: TransactionHash) -> Result, BlockStoreError> { - self.block_store - .block_store - .execution_result_dbs - .get(&self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, key: TransactionHash) -> Result { - self.block_store - .block_store - .execution_result_dbs - .exists(&self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } -} - -impl DataReader> for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read(&self, StateStoreKey(key): StateStoreKey) -> Result>, BlockStoreError> { - self.block_store - .block_store - .read_state_store(&self.txn, &key) - } - - fn exists(&self, StateStoreKey(key): StateStoreKey) -> Result { - self.block_store - .block_store - .state_store_key_exists(&self.txn, &key) - } -} - -impl DataReader<(DbTableId, Vec), DbRawBytesSpec> for IndexedLmdbBlockStoreReadTransaction<'_> { - fn read( - &self, - (id, key): (DbTableId, Vec), - ) -> Result, BlockStoreError> { - if key.is_empty() { - return Ok(None); - } - let store = &self.block_store.block_store; - let res = match id { - DbTableId::BlockHeader => store.block_header_dbs.get_raw(&self.txn, &key), - DbTableId::BlockBody => store.block_body_dbs.get_raw(&self.txn, &key), - DbTableId::ApprovalsHashes => store.approvals_hashes_dbs.get_raw(&self.txn, &key), - DbTableId::BlockMetadata => store.block_metadata_dbs.get_raw(&self.txn, &key), - DbTableId::Transaction => store.transaction_dbs.get_raw(&self.txn, &key), - DbTableId::ExecutionResult => store.execution_result_dbs.get_raw(&self.txn, &key), - DbTableId::Transfer => store.transfer_dbs.get_raw(&self.txn, &key), - DbTableId::FinalizedTransactionApprovals => store - .finalized_transaction_approvals_dbs - .get_raw(&self.txn, &key), - }; - res.map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, key: (DbTableId, Vec)) -> Result { - self.read(key).map(|res| res.is_some()) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - /// Writes a block to storage. - /// - /// Returns `Ok(true)` if the block has been successfully written, `Ok(false)` if a part of it - /// couldn't be written because it already existed, and `Err(_)` if there was an error. - fn write(&mut self, data: &Block) -> Result { - let block_header = data.clone_header(); - let block_hash = data.hash(); - let block_height = data.height(); - let era_id = data.era_id(); - let transaction_hashes: Vec = match &data { - Block::V1(v1) => v1 - .deploy_and_transfer_hashes() - .map(TransactionHash::from) - .collect(), - Block::V2(v2) => v2.all_transactions().copied().collect(), - }; - - let update_height_index = - self.should_update_block_height_index(block_height, block_hash)?; - let update_switch_block_index = self.should_update_switch_block_index(&block_header)?; - let update_transaction_hash_index = - self.should_update_transaction_hash_index(&transaction_hashes, block_hash)?; - - let key = self.block_store.write_block(&mut self.txn, data)?; - - if update_height_index { - self.block_height_index.insert(block_height, *block_hash); - } - - if update_switch_block_index { - self.switch_block_era_id_index.insert(era_id, *block_hash); - } - - if update_transaction_hash_index { - for hash in transaction_hashes { - self.transaction_hash_index.insert( - hash, - BlockHashHeightAndEra::new(*block_hash, block_height, era_id), - ); - } - } - - Ok(key) - } - - fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - let maybe_block = self.block_store.get_single_block(&self.txn, &key)?; - - if let Some(block) = maybe_block { - let transaction_hashes: Vec = match &block { - Block::V1(v1) => v1 - .deploy_and_transfer_hashes() - .map(TransactionHash::from) - .collect(), - Block::V2(v2) => v2.all_transactions().copied().collect(), - }; - - self.block_store.delete_block_header(&mut self.txn, &key)?; - - /* - TODO: currently we don't delete the block body since other blocks may reference it. - self.block_store - .delete_block_body(&mut self.txn, block.body_hash())?; - */ - - self.block_height_index.remove(block.height()); - - if block.is_switch_block() { - self.switch_block_era_id_index.remove(block.era_id()); - } - - for hash in transaction_hashes { - self.transaction_hash_index.remove(hash); - } - - self.block_store - .delete_finality_signatures(&mut self.txn, &key)?; - } - Ok(()) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &ApprovalsHashes) -> Result { - self.block_store.write_approvals_hashes(&mut self.txn, data) - } - - fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - self.block_store - .delete_approvals_hashes(&mut self.txn, &key) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &BlockSignatures) -> Result { - self.block_store - .write_finality_signatures(&mut self.txn, data) - } - - fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - self.block_store - .delete_finality_signatures(&mut self.txn, &key) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &BlockHeader) -> Result { - let block_hash = data.block_hash(); - let block_height = data.height(); - let era_id = data.era_id(); - - let update_height_index = - self.should_update_block_height_index(block_height, &block_hash)?; - let update_switch_block_index = self.should_update_switch_block_index(data)?; - - let key = self.block_store.write_block_header(&mut self.txn, data)?; - - if update_height_index { - self.block_height_index.insert(block_height, block_hash); - } - - if update_switch_block_index { - self.switch_block_era_id_index.insert(era_id, block_hash); - } - - Ok(key) - } - - fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - let maybe_block_header = self.block_store.get_single_block_header(&self.txn, &key)?; - - if let Some(block_header) = maybe_block_header { - self.block_store.delete_block_header(&mut self.txn, &key)?; - - if block_header.is_switch_block() { - self.switch_block_era_id_index.remove(block_header.era_id()); - } - - self.block_height_index.remove(block_header.height()); - } - Ok(()) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &Transaction) -> Result { - self.block_store.write_transaction(&mut self.txn, data) - } - - fn delete(&mut self, key: TransactionHash) -> Result<(), BlockStoreError> { - self.block_store.delete_transaction(&mut self.txn, &key) - } -} - -impl DataWriter - for IndexedLmdbBlockStoreRWTransaction<'_> -{ - fn write( - &mut self, - data: &TransactionFinalizedApprovals, - ) -> Result { - self.block_store - .finalized_transaction_approvals_dbs - .put( - &mut self.txn, - &data.transaction_hash, - &data.finalized_approvals, - true, - ) - .map(|_| data.transaction_hash) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn delete(&mut self, key: TransactionHash) -> Result<(), BlockStoreError> { - self.block_store - .finalized_transaction_approvals_dbs - .delete(&mut self.txn, &key) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } -} - -impl DataWriter - for IndexedLmdbBlockStoreRWTransaction<'_> -{ - fn write( - &mut self, - data: &BlockExecutionResults, - ) -> Result { - let transaction_hashes: Vec = data.exec_results.keys().copied().collect(); - let block_hash = data.block_info.block_hash; - let block_height = data.block_info.block_height; - let era_id = data.block_info.era_id; - - let update_transaction_hash_index = - self.should_update_transaction_hash_index(&transaction_hashes, &block_hash)?; - - let _ = self.block_store.write_execution_results( - &mut self.txn, - &block_hash, - data.exec_results.clone(), - )?; - - if update_transaction_hash_index { - for hash in transaction_hashes { - self.transaction_hash_index.insert( - hash, - BlockHashHeightAndEra::new(block_hash, block_height, era_id), - ); - } - } - - Ok(data.block_info) - } - - fn delete(&mut self, _key: BlockHashHeightAndEra) -> Result<(), BlockStoreError> { - Err(BlockStoreError::UnsupportedOperation) - } -} - -impl DataWriter for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &BlockTransfers) -> Result { - self.block_store - .write_transfers(&mut self.txn, &data.block_hash, &data.transfers) - .map(|_| data.block_hash) - } - - fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - self.block_store.delete_transfers(&mut self.txn, &key) - } -} - -impl DataWriter, StateStore> for IndexedLmdbBlockStoreRWTransaction<'_> { - fn write(&mut self, data: &StateStore) -> Result, BlockStoreError> { - self.block_store - .write_state_store(&mut self.txn, data.key.clone(), &data.value)?; - Ok(data.key.clone()) - } - - fn delete(&mut self, key: Cow<'static, [u8]>) -> Result<(), BlockStoreError> { - self.block_store.delete_state_store(&mut self.txn, key) - } -} - -impl DataReader for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, query: TransactionHash) -> Result, BlockStoreError> { - self.block_store - .transaction_dbs - .get(&self.txn, &query) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, query: TransactionHash) -> Result { - self.block_store.transaction_exists(&self.txn, &query) - } -} - -impl DataReader for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store.get_block_signatures(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store.block_signatures_exist(&self.txn, &key) - } -} - -impl DataReader> for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, query: TransactionHash) -> Result>, BlockStoreError> { - self.block_store - .finalized_transaction_approvals_dbs - .get(&self.txn, &query) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, query: TransactionHash) -> Result { - self.block_store - .finalized_transaction_approvals_dbs - .exists(&self.txn, &query) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } -} - -impl DataReader for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store.get_single_block(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store.block_exists(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, key: BlockHash) -> Result, BlockStoreError> { - self.block_store.get_single_block_header(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store.block_header_exists(&self.txn, &key) - } -} - -impl DataReader for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, query: TransactionHash) -> Result, BlockStoreError> { - self.block_store - .execution_result_dbs - .get(&self.txn, &query) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } - - fn exists(&self, query: TransactionHash) -> Result { - self.block_store - .execution_result_dbs - .exists(&self.txn, &query) - .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) - } -} - -impl DataReader> for IndexedLmdbBlockStoreRWTransaction<'_> { - fn read(&self, key: BlockHash) -> Result>, BlockStoreError> { - self.block_store.get_transfers(&self.txn, &key) - } - - fn exists(&self, key: BlockHash) -> Result { - self.block_store.has_transfers(&self.txn, &key) - } -} diff --git a/storage/src/block_store/lmdb/lmdb_block_store.rs b/storage/src/block_store/lmdb/lmdb_block_store.rs index 4e975fb2e1..7601ef7b22 100644 --- a/storage/src/block_store/lmdb/lmdb_block_store.rs +++ b/storage/src/block_store/lmdb/lmdb_block_store.rs @@ -1,33 +1,39 @@ use std::{ borrow::Cow, - collections::{BTreeSet, HashMap}, + collections::{btree_map, BTreeMap, BTreeSet, HashMap}, path::{Path, PathBuf}, sync::Arc, }; use datasize::DataSize; -use tracing::{debug, error}; +use tracing::{debug, error, info}; use casper_types::{ execution::{execution_result_v1, ExecutionResult, ExecutionResultV1}, - Approval, Block, BlockBody, BlockHash, BlockHeader, BlockSignatures, Digest, Transaction, - TransactionHash, Transfer, + Approval, Block, BlockBody, BlockHash, BlockHeader, BlockSignatures, Digest, EraId, + Transaction, TransactionHash, Transfer, }; use super::{ - lmdb_ext::{LmdbExtError, TransactionExt}, + lmdb_ext::{ + append_by_be_u64_key, append_value_bytesrepr, delete_by_be_u64_key, delete_value_bytesrepr, + get_by_be_u64_key, get_last_by_be_u64_key, put_by_be_u64_key, LmdbExtError, TransactionExt, + WriteTransactionExt, + }, versioned_databases::VersionedDatabases, + DbTableId, }; use crate::block_store::{ error::BlockStoreError, types::{ - ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockTransfers, StateStore, - TransactionFinalizedApprovals, Transfers, + ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockHeight, BlockTransfers, + LatestSwitchBlock, StateStore, StateStoreKey, Tip, TransactionFinalizedApprovals, + Transfers, }, - BlockStoreProvider, BlockStoreTransaction, DataReader, DataWriter, + BlockStoreProvider, BlockStoreTransaction, DataReader, DataWriter, DbRawBytesSpec, }; use lmdb::{ - Database, DatabaseFlags, Environment, EnvironmentFlags, RoTransaction, RwTransaction, + Database, DatabaseFlags, Environment, EnvironmentFlags, RoTransaction, RwCursor, RwTransaction, Transaction as LmdbTransaction, WriteFlags, }; @@ -39,7 +45,7 @@ const STORAGE_DB_FILENAME: &str = "storage.lmdb"; const MAX_TRANSACTIONS: u32 = 5; /// Maximum number of allowed dbs. -const MAX_DB_COUNT: u32 = 17; +const MAX_DB_COUNT: u32 = 20; /// OS-specific lmdb flags. #[cfg(not(target_os = "macos"))] @@ -80,6 +86,16 @@ pub struct LmdbBlockStore { /// The finalized transaction approvals databases. pub(super) finalized_transaction_approvals_dbs: VersionedDatabases>, + /// Disk-backed index of block height to block hash. + #[data_size(skip)] + pub(super) block_height_index_db: Database, + /// Disk-backed index of era ID to switch block hash. + #[data_size(skip)] + pub(super) switch_block_era_id_index_db: Database, + /// Disk-backed index of transaction hash to the hash, height and era of the block containing + /// it. + #[data_size(skip)] + pub(super) transaction_hash_index_db: Database, } impl LmdbBlockStore { @@ -114,6 +130,16 @@ impl LmdbBlockStore { VersionedDatabases::new(&env, "approvals_hashes", "versioned_approvals_hashes") .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + let block_height_index_db = env + .create_db(Some("block_height_index"), DatabaseFlags::empty()) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + let switch_block_era_id_index_db = env + .create_db(Some("switch_block_era_id_index"), DatabaseFlags::empty()) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + let transaction_hash_index_db = env + .create_db(Some("transaction_hash_index"), DatabaseFlags::empty()) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + Ok(Self { root: root_path.to_path_buf(), env: Arc::new(env), @@ -126,9 +152,255 @@ impl LmdbBlockStore { transfer_dbs, state_store_db, finalized_transaction_approvals_dbs, + block_height_index_db, + switch_block_era_id_index_db, + transaction_hash_index_db, }) } + /// Initializes the disk-backed indexes. This operation can be time + /// consuming because it needs to go through all entries in block + /// headers db. If the index has data it assumes that no reindexing + /// is needed. + pub fn init(&mut self) -> Result<(), BlockStoreError> { + let ro_txn = self + .env + .begin_ro_txn() + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + let index_is_empty = ro_txn + .stat(self.block_height_index_db) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + .entries() + == 0; + let headers_exist = header_count(&ro_txn, self)? > 0; + ro_txn + .commit() + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + if headers_exist && index_is_empty { + info!("block store indexes appear to be missing; building them from a full scan"); + self.rebuild_indexes()?; + } + + Ok(()) + } + + /// Performs an unconditional one-off full rebuild of the disk-backed block-height/ + /// switch-block-era-id/transaction-hash indexes, by scanning every block header currently in + /// storage. Exposed for tests; startup code should use [`Self::init`], which only rebuilds + /// when necessary. + #[cfg(test)] + pub fn reindex(&mut self) -> Result<(), BlockStoreError> { + self.rebuild_indexes() + } + + fn rebuild_indexes(&mut self) -> Result<(), BlockStoreError> { + info!("reindexing block store"); + + let mut block_height_index = BTreeMap::new(); + let mut switch_block_era_id_index = BTreeMap::new(); + let mut transaction_hash_index = BTreeMap::new(); + + let mut block_txn = self + .env + .begin_rw_txn() + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + let total_headers = header_count(&block_txn, self)?; + let progress_step = (total_headers / 20).max(1); + let mut processed: usize = 0; + + // First pass: scan every header through the cursor to build the height/switch-block + // indexes and collect the headers for the second pass below. + let mut headers = Vec::new(); + let mut collect_fn = + |_cursor: &mut RwCursor, block_header: BlockHeader| -> Result<(), BlockStoreError> { + processed += 1; + if processed.is_multiple_of(progress_step) { + info!( + percent_complete = (processed * 100 / total_headers.max(1)), + processed, total_headers, "reindexing block store: scanning headers" + ); + } + + Self::insert_to_block_header_indices( + &mut block_height_index, + &mut switch_block_era_id_index, + &block_header, + )?; + headers.push(block_header); + + Ok(()) + }; + + self.block_header_dbs + .for_each_value_in_current(&mut block_txn, &mut collect_fn)?; + self.block_header_dbs + .for_each_value_in_legacy(&mut block_txn, &mut collect_fn)?; + + // Second pass: the cursor's borrow of `block_txn` has ended, so each block body can be + // read through the transaction we already have open, instead of a fresh one per header. + let total_bodies = headers.len(); + let body_progress_step = (total_bodies / 20).max(1); + for (processed, block_header) in headers.iter().enumerate() { + if processed.is_multiple_of(body_progress_step) { + info!( + percent_complete = (processed * 100 / total_bodies.max(1)), + processed, total_bodies, "reindexing block store: scanning bodies" + ); + } + + let maybe_block_body = self + .block_body_dbs + .get(&block_txn, block_header.body_hash()) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + if let Some(block_body) = &maybe_block_body { + let transaction_hashes = block_transaction_hashes(block_body); + Self::insert_to_transaction_index( + &mut transaction_hash_index, + block_header.block_hash(), + block_header.height(), + block_header.era_id(), + transaction_hashes, + )?; + } + } + + // The scan above makes no changes to the header dbs (unlike `prune`), so this can just be + // rolled back rather than committed. + block_txn.abort(); + + let mut index_txn = self + .env + .begin_rw_txn() + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + index_txn + .clear_db(self.block_height_index_db) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + index_txn + .clear_db(self.switch_block_era_id_index_db) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + index_txn + .clear_db(self.transaction_hash_index_db) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + // `block_height_index`/`switch_block_era_id_index`/`transaction_hash_index` are + // `BTreeMap`s, so iterating them yields ascending key order; combined with the `clear_db` + // calls above, this lets us use LMDB's `APPEND` flag to skip the usual B-tree + // search/rebalance per insert (a significant speedup for a full rebuild). This is safe + // because: the two `u64`/`EraId`-keyed indexes use `append_by_be_u64_key`, whose + // big-endian key encoding is specifically chosen so ascending numeric order is ascending + // byte order; and `TransactionHash`'s derived `Ord` (variant tag, then digest bytes) + // matches its `bytesrepr` encoding (tag byte, then raw digest bytes) byte-for-byte. + for (height, block_hash) in block_height_index { + append_by_be_u64_key( + &mut index_txn, + self.block_height_index_db, + height, + &block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + for (era_id, block_hash) in switch_block_era_id_index { + append_by_be_u64_key( + &mut index_txn, + self.switch_block_era_id_index_db, + era_id.value(), + &block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + for (transaction_hash, block_info) in transaction_hash_index { + append_value_bytesrepr( + &mut index_txn, + self.transaction_hash_index_db, + &transaction_hash, + &block_info, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + index_txn + .commit() + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + info!("block store reindexing complete"); + Ok(()) + } + + /// Inserts the relevant entries to the index. + /// + /// If a duplicate entry is encountered, index is not updated and an error is returned. + fn insert_to_transaction_index( + transaction_hash_index: &mut BTreeMap, + block_hash: BlockHash, + block_height: u64, + era_id: EraId, + transaction_hashes: Vec, + ) -> Result<(), BlockStoreError> { + if let Some(hash) = transaction_hashes.iter().find(|hash| { + transaction_hash_index + .get(hash) + .is_some_and(|old_details| old_details.block_hash != block_hash) + }) { + return Err(BlockStoreError::DuplicateTransaction { + transaction_hash: *hash, + first: transaction_hash_index[hash].block_hash, + second: block_hash, + }); + } + + for hash in transaction_hashes { + transaction_hash_index.insert( + hash, + BlockHashHeightAndEra::new(block_hash, block_height, era_id), + ); + } + + Ok(()) + } + + /// Inserts the relevant entries to the two indices. + /// + /// If a duplicate entry is encountered, neither index is updated and an error is returned. + fn insert_to_block_header_indices( + block_height_index: &mut BTreeMap, + switch_block_era_id_index: &mut BTreeMap, + block_header: &BlockHeader, + ) -> Result<(), BlockStoreError> { + let block_hash = block_header.block_hash(); + if let Some(first) = block_height_index.get(&block_header.height()) { + if *first != block_hash { + return Err(BlockStoreError::DuplicateBlock { + height: block_header.height(), + first: *first, + second: block_hash, + }); + } + } + + if block_header.is_switch_block() { + match switch_block_era_id_index.entry(block_header.era_id()) { + btree_map::Entry::Vacant(entry) => { + let _ = entry.insert(block_hash); + } + btree_map::Entry::Occupied(entry) => { + if *entry.get() != block_hash { + return Err(BlockStoreError::DuplicateEraId { + era_id: block_header.era_id(), + first: *entry.get(), + second: block_hash, + }); + } + } + } + } + + let _ = block_height_index.insert(block_header.height(), block_hash); + Ok(()) + } + /// Write finality signatures. pub fn write_finality_signatures( &self, @@ -555,6 +827,32 @@ impl LmdbBlockStore { } } +fn header_count( + txn: &Tx, + block_store: &LmdbBlockStore, +) -> Result { + let current = txn + .stat(block_store.block_header_dbs.current) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + .entries(); + let legacy = txn + .stat(block_store.block_header_dbs.legacy) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + .entries(); + Ok(current + legacy) +} + +/// Returns the transaction hashes referenced by a block body. +fn block_transaction_hashes(block_body: &BlockBody) -> Vec { + match block_body { + BlockBody::V1(v1) => v1 + .deploy_and_transfer_hashes() + .map(TransactionHash::from) + .collect(), + BlockBody::V2(v2) => v2.all_transactions().copied().collect(), + } +} + pub(crate) fn new_environment( total_size: usize, root: &Path, @@ -793,24 +1091,602 @@ where } } +enum LmdbBlockStoreIndex { + BlockHeight(IndexPosition), + SwitchBlockEraId(IndexPosition), +} + +enum IndexPosition { + Tip, + Key(K), +} + +enum DataType { + Block, + BlockHeader, + ApprovalsHashes, + BlockSignatures, +} + +impl LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn block_hash_from_index( + &self, + index: LmdbBlockStoreIndex, + ) -> Result, BlockStoreError> { + let result = match index { + LmdbBlockStoreIndex::BlockHeight(position) => match position { + IndexPosition::Tip => get_last_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.block_height_index_db, + ), + IndexPosition::Key(height) => get_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.block_height_index_db, + height, + ), + }, + LmdbBlockStoreIndex::SwitchBlockEraId(position) => match position { + IndexPosition::Tip => get_last_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.switch_block_era_id_index_db, + ), + IndexPosition::Key(era_id) => get_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.switch_block_era_id_index_db, + era_id.value(), + ), + }, + }; + result.map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) + } + + fn read_block_indexed( + &self, + index: LmdbBlockStoreIndex, + ) -> Result, BlockStoreError> { + match self.block_hash_from_index(index)? { + Some(block_hash) => self.block_store.get_single_block(&self.txn, &block_hash), + None => Ok(None), + } + } + + fn read_block_header_indexed( + &self, + index: LmdbBlockStoreIndex, + ) -> Result, BlockStoreError> { + match self.block_hash_from_index(index)? { + Some(block_hash) => self + .block_store + .get_single_block_header(&self.txn, &block_hash), + None => Ok(None), + } + } + + fn read_block_signatures_indexed( + &self, + index: LmdbBlockStoreIndex, + ) -> Result, BlockStoreError> { + match self.block_hash_from_index(index)? { + Some(block_hash) => self + .block_store + .get_block_signatures(&self.txn, &block_hash), + None => Ok(None), + } + } + + fn read_approvals_hashes_indexed( + &self, + index: LmdbBlockStoreIndex, + ) -> Result, BlockStoreError> { + match self.block_hash_from_index(index)? { + Some(block_hash) => self + .block_store + .read_approvals_hashes(&self.txn, &block_hash), + None => Ok(None), + } + } + + fn contains_data_indexed( + &self, + index: LmdbBlockStoreIndex, + data_type: DataType, + ) -> Result { + match self.block_hash_from_index(index)? { + Some(block_hash) => match data_type { + DataType::Block => self.block_store.block_exists(&self.txn, &block_hash), + DataType::BlockHeader => { + self.block_store.block_header_exists(&self.txn, &block_hash) + } + DataType::ApprovalsHashes => self + .block_store + .approvals_hashes_exist(&self.txn, &block_hash), + DataType::BlockSignatures => self + .block_store + .block_signatures_exist(&self.txn, &block_hash), + }, + None => Ok(false), + } + } + + /// Returns the height of the switch block for the given era, if known. + pub fn get_switch_block_height(&self, era_id: EraId) -> Result, BlockStoreError> { + let index = LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(era_id)); + match self.block_hash_from_index(index)? { + Some(block_hash) => { + let maybe_header: Option = self.read(block_hash)?; + Ok(maybe_header.map(|header| header.height())) + } + None => Ok(None), + } + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { + self.read_block_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key))) + } + + fn exists(&self, key: BlockHeight) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), + DataType::Block, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { + self.read_block_header_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key))) + } + + fn exists(&self, key: BlockHeight) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), + DataType::BlockHeader, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { + self.read_approvals_hashes_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key( + key, + ))) + } + + fn exists(&self, key: BlockHeight) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), + DataType::ApprovalsHashes, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: BlockHeight) -> Result, BlockStoreError> { + self.read_block_signatures_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key( + key, + ))) + } + + fn exists(&self, key: BlockHeight) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Key(key)), + DataType::BlockSignatures, + ) + } +} + +/// Retrieves single switch block by era ID by looking it up in the index and returning it. +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: EraId) -> Result, BlockStoreError> { + self.read_block_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key( + key, + ))) + } + + fn exists(&self, key: EraId) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), + DataType::Block, + ) + } +} + +/// Retrieves single switch block header by era ID by looking it up in the index and returning +/// it. +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: EraId) -> Result, BlockStoreError> { + self.read_block_header_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key( + key, + ))) + } + + fn exists(&self, key: EraId) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), + DataType::BlockHeader, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: EraId) -> Result, BlockStoreError> { + self.read_approvals_hashes_indexed(LmdbBlockStoreIndex::SwitchBlockEraId( + IndexPosition::Key(key), + )) + } + + fn exists(&self, key: EraId) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), + DataType::ApprovalsHashes, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: EraId) -> Result, BlockStoreError> { + self.read_block_signatures_indexed(LmdbBlockStoreIndex::SwitchBlockEraId( + IndexPosition::Key(key), + )) + } + + fn exists(&self, key: EraId) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(key)), + DataType::BlockSignatures, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, _key: Tip) -> Result, BlockStoreError> { + self.read_block_header_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip)) + } + + fn exists(&self, _key: Tip) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip), + DataType::BlockHeader, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, _key: Tip) -> Result, BlockStoreError> { + self.read_block_indexed(LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip)) + } + + fn exists(&self, _key: Tip) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::BlockHeight(IndexPosition::Tip), + DataType::Block, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, _key: LatestSwitchBlock) -> Result, BlockStoreError> { + self.read_block_header_indexed(LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Tip)) + } + + fn exists(&self, _key: LatestSwitchBlock) -> Result { + self.contains_data_indexed( + LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Tip), + DataType::BlockHeader, + ) + } +} + +impl DataReader for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, key: TransactionHash) -> Result, BlockStoreError> { + self.txn + .get_value_bytesrepr(self.block_store.transaction_hash_index_db, &key) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) + } + + fn exists(&self, key: TransactionHash) -> Result { + self.txn + .value_exists_bytesrepr(self.block_store.transaction_hash_index_db, &key) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) + } +} + +impl DataReader> for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read(&self, StateStoreKey(key): StateStoreKey) -> Result>, BlockStoreError> { + self.block_store.read_state_store(&self.txn, &key) + } + + fn exists(&self, StateStoreKey(key): StateStoreKey) -> Result { + self.block_store.state_store_key_exists(&self.txn, &key) + } +} + +impl DataReader<(DbTableId, Vec), DbRawBytesSpec> for LmdbBlockStoreTransaction<'_, T> +where + T: LmdbTransaction, +{ + fn read( + &self, + (id, key): (DbTableId, Vec), + ) -> Result, BlockStoreError> { + if key.is_empty() { + return Ok(None); + } + let store = self.block_store; + let res = match id { + DbTableId::BlockHeader => store.block_header_dbs.get_raw(&self.txn, &key), + DbTableId::BlockBody => store.block_body_dbs.get_raw(&self.txn, &key), + DbTableId::ApprovalsHashes => store.approvals_hashes_dbs.get_raw(&self.txn, &key), + DbTableId::BlockMetadata => store.block_metadata_dbs.get_raw(&self.txn, &key), + DbTableId::Transaction => store.transaction_dbs.get_raw(&self.txn, &key), + DbTableId::ExecutionResult => store.execution_result_dbs.get_raw(&self.txn, &key), + DbTableId::Transfer => store.transfer_dbs.get_raw(&self.txn, &key), + DbTableId::FinalizedTransactionApprovals => store + .finalized_transaction_approvals_dbs + .get_raw(&self.txn, &key), + }; + res.map_err(|err| BlockStoreError::InternalStorage(Box::new(err))) + } + + fn exists(&self, key: (DbTableId, Vec)) -> Result { + self.read(key).map(|res| res.is_some()) + } +} + +impl<'t> LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { + /// Check if the block height index can be updated. + fn should_update_block_height_index( + &self, + block_height: u64, + block_hash: &BlockHash, + ) -> Result { + match get_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.block_height_index_db, + block_height, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + { + // There is a block in the index at this height + Some(first) if first != *block_hash => Err(BlockStoreError::DuplicateBlock { + height: block_height, + first, + second: *block_hash, + }), + // Same value already in index, no need to update it. + Some(_) => Ok(false), + // Value not in index, update. + None => Ok(true), + } + } + + /// Check if the switch block index can be updated. + fn should_update_switch_block_index( + &self, + block_header: &BlockHeader, + ) -> Result { + if !block_header.is_switch_block() { + return Ok(false); + } + let era_id = block_header.era_id(); + match get_by_be_u64_key::<_, BlockHash>( + &self.txn, + self.block_store.switch_block_era_id_index_db, + era_id.value(), + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + { + Some(entry) if entry != block_header.block_hash() => { + Err(BlockStoreError::DuplicateEraId { + era_id, + first: entry, + second: block_header.block_hash(), + }) + } + // already in index, no need to update. + Some(_) => Ok(false), + // not in the index, update. + None => Ok(true), + } + } + + // Check if the transaction hash index can be updated. + fn should_update_transaction_hash_index( + &self, + transaction_hashes: &[TransactionHash], + block_hash: &BlockHash, + ) -> Result { + for hash in transaction_hashes { + if let Some(old_details) = self + .txn + .get_value_bytesrepr::<_, BlockHashHeightAndEra>( + self.block_store.transaction_hash_index_db, + hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? + { + if old_details.block_hash != *block_hash { + return Err(BlockStoreError::DuplicateTransaction { + transaction_hash: *hash, + first: old_details.block_hash, + second: *block_hash, + }); + } + } + } + Ok(true) + } +} + impl<'t> DataWriter for LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { /// Writes a block to storage. + /// + /// Returns `Ok(true)` if the block has been successfully written, `Ok(false)` if a part of it + /// couldn't be written because it already existed, and `Err(_)` if there was an error. fn write(&mut self, data: &Block) -> Result { - self.block_store.write_block(&mut self.txn, data) + let block_header = data.clone_header(); + let block_hash = data.hash(); + let block_height = data.height(); + let era_id = data.era_id(); + let transaction_hashes: Vec = match &data { + Block::V1(v1) => v1 + .deploy_and_transfer_hashes() + .map(TransactionHash::from) + .collect(), + Block::V2(v2) => v2.all_transactions().copied().collect(), + }; + + let update_height_index = + self.should_update_block_height_index(block_height, block_hash)?; + let update_switch_block_index = self.should_update_switch_block_index(&block_header)?; + let update_transaction_hash_index = + self.should_update_transaction_hash_index(&transaction_hashes, block_hash)?; + + let key = self.block_store.write_block(&mut self.txn, data)?; + + if update_height_index { + put_by_be_u64_key( + &mut self.txn, + self.block_store.block_height_index_db, + block_height, + block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + if update_switch_block_index { + put_by_be_u64_key( + &mut self.txn, + self.block_store.switch_block_era_id_index_db, + era_id.value(), + block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + if update_transaction_hash_index { + for hash in transaction_hashes { + self.txn + .put_value_bytesrepr( + self.block_store.transaction_hash_index_db, + &hash, + &BlockHashHeightAndEra::new(*block_hash, block_height, era_id), + true, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + } + + Ok(key) } fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - let maybe_block = self.block_store.get_single_block_header(&self.txn, &key)?; + let maybe_block = self.block_store.get_single_block(&self.txn, &key)?; + + if let Some(block) = maybe_block { + let transaction_hashes: Vec = match &block { + Block::V1(v1) => v1 + .deploy_and_transfer_hashes() + .map(TransactionHash::from) + .collect(), + Block::V2(v2) => v2.all_transactions().copied().collect(), + }; - if let Some(block_header) = maybe_block { self.block_store.delete_block_header(&mut self.txn, &key)?; + self.block_store - .delete_block_body(&mut self.txn, block_header.body_hash())?; + .delete_block_body(&mut self.txn, block.body_hash())?; + + delete_by_be_u64_key( + &mut self.txn, + self.block_store.block_height_index_db, + block.height(), + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + + if block.is_switch_block() { + delete_by_be_u64_key( + &mut self.txn, + self.block_store.switch_block_era_id_index_db, + block.era_id().value(), + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + for hash in transaction_hashes { + delete_value_bytesrepr( + &mut self.txn, + self.block_store.transaction_hash_index_db, + &hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + self.block_store + .delete_finality_signatures(&mut self.txn, &key)?; } Ok(()) } } +impl<'t> DataWriter for LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { + /// Not supported: a block body is always written together with its header, as part of + /// writing a whole `Block` (see the `DataWriter` impl above). + fn write(&mut self, _data: &BlockBody) -> Result { + Err(BlockStoreError::UnsupportedOperation) + } + + /// Deletes a block body by its hash. Callers are responsible for only doing so once no + /// retained block header still references this body hash. + fn delete(&mut self, key: Digest) -> Result<(), BlockStoreError> { + self.block_store.delete_block_body(&mut self.txn, &key) + } +} + impl<'t> DataWriter for LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { @@ -840,11 +1716,62 @@ impl<'t> DataWriter impl<'t> DataWriter for LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { fn write(&mut self, data: &BlockHeader) -> Result { - self.block_store.write_block_header(&mut self.txn, data) + let block_hash = data.block_hash(); + let block_height = data.height(); + let era_id = data.era_id(); + + let update_height_index = + self.should_update_block_height_index(block_height, &block_hash)?; + let update_switch_block_index = self.should_update_switch_block_index(data)?; + + let key = self.block_store.write_block_header(&mut self.txn, data)?; + + if update_height_index { + put_by_be_u64_key( + &mut self.txn, + self.block_store.block_height_index_db, + block_height, + &block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + if update_switch_block_index { + put_by_be_u64_key( + &mut self.txn, + self.block_store.switch_block_era_id_index_db, + era_id.value(), + &block_hash, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + Ok(key) } fn delete(&mut self, key: BlockHash) -> Result<(), BlockStoreError> { - self.block_store.delete_block_header(&mut self.txn, &key) + let maybe_block_header = self.block_store.get_single_block_header(&self.txn, &key)?; + + if let Some(block_header) = maybe_block_header { + self.block_store.delete_block_header(&mut self.txn, &key)?; + + if block_header.is_switch_block() { + delete_by_be_u64_key( + &mut self.txn, + self.block_store.switch_block_era_id_index_db, + block_header.era_id().value(), + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + + delete_by_be_u64_key( + &mut self.txn, + self.block_store.block_height_index_db, + block_header.height(), + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + Ok(()) } } @@ -922,7 +1849,13 @@ impl<'t> DataWriter &mut self, data: &BlockExecutionResults, ) -> Result { + let transaction_hashes: Vec = data.exec_results.keys().copied().collect(); let block_hash = data.block_info.block_hash; + let block_height = data.block_info.block_height; + let era_id = data.block_info.era_id; + + let update_transaction_hash_index = + self.should_update_transaction_hash_index(&transaction_hashes, &block_hash)?; let _ = self.block_store.write_execution_results( &mut self.txn, @@ -930,6 +1863,19 @@ impl<'t> DataWriter data.exec_results.clone(), )?; + if update_transaction_hash_index { + for hash in transaction_hashes { + self.txn + .put_value_bytesrepr( + self.block_store.transaction_hash_index_db, + &hash, + &BlockHashHeightAndEra::new(block_hash, block_height, era_id), + true, + ) + .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))?; + } + } + Ok(data.block_info) } @@ -942,3 +1888,221 @@ impl<'t> DataWriter Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use casper_types::{ + testing::TestRng, BlockHeaderV2, EraEndV2, ProtocolVersion, PublicKey, SecretKey, Timestamp, + }; + use once_cell::sync::OnceCell; + use rand::Rng; + use tempfile::TempDir; + + /// Number of headers to write: deliberately > 256 so that, were the disk-backed indexes ever + /// keyed by plain little-endian `bytesrepr` bytes instead of the big-endian encoding, the + /// `APPEND`-based bulk write in `reindex` would violate LMDB's required key order (since + /// little-endian byte-lexicographic order diverges from numeric order once values exceed a + /// single byte) and fail loudly rather than silently produce a wrong index. + const HEADER_COUNT: u64 = 300; + + fn header_at_height(rng: &mut TestRng, height: u64, proposer: &PublicKey) -> BlockHeader { + let is_switch_block = height % 10 == 9; + let era_id = EraId::new(height / 10); + let era_end = is_switch_block.then(|| EraEndV2::random(rng)); + BlockHeader::V2(BlockHeaderV2::new( + BlockHash::random(rng), + Digest::random(rng), + Digest::hash(height.to_le_bytes()), + rng.gen(), + Digest::random(rng), + era_end, + Timestamp::now(), + era_id, + height, + ProtocolVersion::V1_0_0, + proposer.clone(), + 1, + None, + OnceCell::new(), + )) + } + + #[test] + fn reindex_rebuilds_disk_backed_indexes_via_append() { + let rng = &mut TestRng::new(); + let tempdir = TempDir::new().expect("should create tempdir"); + let mut store = + LmdbBlockStore::new(tempdir.path(), 64 * 1024 * 1024).expect("should create store"); + + let secret_key = SecretKey::random(rng); + let proposer = PublicKey::from(&secret_key); + + let mut headers = Vec::new(); + { + let mut rw_txn = store.checkout_rw().expect("should checkout rw"); + for height in 0..HEADER_COUNT { + let header = header_at_height(rng, height, &proposer); + let _ = DataWriter::::write(&mut rw_txn, &header) + .expect("should write header"); + headers.push(header); + } + rw_txn.commit().expect("should commit"); + } + + store.reindex().expect("reindex should succeed"); + + let ro_txn = store.checkout_ro().expect("should checkout ro"); + + // Spot-check a handful of heights, including ones that require crossing the + // little-endian single-byte boundary (e.g. 255 -> 256) to catch ordering bugs. + for &height in &[0u64, 1, 254, 255, 256, 257, HEADER_COUNT - 1] { + let expected_hash = headers[height as usize].block_hash(); + let actual: Option = ro_txn.read(height).expect("read by height"); + assert_eq!( + actual.expect("header should exist").block_hash(), + expected_hash, + "wrong header at height {height}" + ); + } + + // Tip should be the highest height. + let tip: Option = ro_txn.read(Tip).expect("read tip"); + assert_eq!( + tip.expect("tip should exist").height(), + HEADER_COUNT - 1, + "tip should be the highest height" + ); + + // Switch blocks (height % 10 == 9) should be resolvable by era ID. + for &height in &[9u64, 99, 259, HEADER_COUNT - 1] { + assert_eq!( + height % 10, + 9, + "test bug: {height} is not a switch block height" + ); + let era_id = EraId::new(height / 10); + let expected_hash = headers[height as usize].block_hash(); + let actual: Option = ro_txn.read(era_id).expect("read by era id"); + assert_eq!( + actual + .expect("switch block header should exist") + .block_hash(), + expected_hash, + "wrong switch block header for era {era_id}" + ); + } + + // Latest switch block should be the highest-height header with `is_switch_block()` set + // (derived from `headers` directly, rather than hardcoded, to avoid off-by-one mistakes). + let latest_switch_block_height = headers + .iter() + .filter(|header| header.is_switch_block()) + .map(|header| header.height()) + .max() + .expect("should have at least one switch block"); + let latest_switch: Option = + DataReader::::read(&ro_txn, LatestSwitchBlock) + .expect("read latest switch block"); + assert_eq!( + latest_switch + .expect("latest switch block should exist") + .height(), + latest_switch_block_height, + "wrong latest switch block" + ); + } + + #[test] + fn init_builds_index_when_headers_exist_but_index_is_empty() { + let rng = &mut TestRng::new(); + let tempdir = TempDir::new().expect("should create tempdir"); + let mut store = + LmdbBlockStore::new(tempdir.path(), 64 * 1024 * 1024).expect("should create store"); + + let secret_key = SecretKey::random(rng); + let proposer = PublicKey::from(&secret_key); + + // Write a header directly via a raw transaction, bypassing the index-maintaining + // `DataWriter` impl -- simulating a migration from a binary version that didn't yet + // maintain these disk-backed indexes. + let header = header_at_height(rng, 0, &proposer); + { + let mut txn = store.env.begin_rw_txn().expect("should begin rw txn"); + let _ = store + .write_block_header(&mut txn, &header) + .expect("should write header"); + txn.commit().expect("should commit"); + } + + // The index hasn't been told about this header yet. + { + let ro_txn = store.checkout_ro().expect("should checkout ro"); + let by_height: Option = ro_txn.read(0u64).expect("read by height"); + assert!(by_height.is_none(), "index should not exist yet"); + } + + store.init().expect("init should succeed"); + + let ro_txn = store.checkout_ro().expect("should checkout ro"); + let by_height: Option = ro_txn.read(0u64).expect("read by height"); + assert_eq!( + by_height + .expect("header should be indexed after init") + .block_hash(), + header.block_hash(), + "init should have built the height index from the existing headers" + ); + } + + #[test] + fn init_does_not_rebuild_an_already_populated_index() { + let rng = &mut TestRng::new(); + let tempdir = TempDir::new().expect("should create tempdir"); + let mut store = + LmdbBlockStore::new(tempdir.path(), 64 * 1024 * 1024).expect("should create store"); + + let secret_key = SecretKey::random(rng); + let proposer = PublicKey::from(&secret_key); + + // Write two headers through the normal, index-maintaining path. + let headers: Vec = (0..2) + .map(|height| header_at_height(rng, height, &proposer)) + .collect(); + { + let mut rw_txn = store.checkout_rw().expect("should checkout rw"); + for header in &headers { + let _ = DataWriter::::write(&mut rw_txn, header) + .expect("should write header"); + } + rw_txn.commit().expect("should commit"); + } + + // Directly corrupt the height index by deleting the entry for height 0, without touching + // the header itself -- an inconsistency that only a full rebuild would fix. + { + let mut index_txn = store.env.begin_rw_txn().expect("should begin rw txn"); + delete_by_be_u64_key(&mut index_txn, store.block_height_index_db, 0) + .expect("should delete index entry"); + index_txn.commit().expect("should commit"); + } + + store.init().expect("init should succeed"); + + // Since the index wasn't empty (height 1's entry is still present), `init` must have + // skipped rebuilding it -- so the deleted entry for height 0 stays missing. + let ro_txn = store.checkout_ro().expect("should checkout ro"); + let by_height_0: Option = ro_txn.read(0u64).expect("read by height"); + assert!( + by_height_0.is_none(), + "init should not have rebuilt an already-populated index" + ); + let by_height_1: Option = ro_txn.read(1u64).expect("read by height"); + assert_eq!( + by_height_1 + .expect("height 1 should still be indexed") + .block_hash(), + headers[1].block_hash() + ); + } +} diff --git a/storage/src/block_store/lmdb/lmdb_ext.rs b/storage/src/block_store/lmdb/lmdb_ext.rs index 3f965f1270..6da614a3c8 100644 --- a/storage/src/block_store/lmdb/lmdb_ext.rs +++ b/storage/src/block_store/lmdb/lmdb_ext.rs @@ -12,7 +12,8 @@ use std::{any::TypeId, collections::BTreeSet}; -use lmdb::{Database, RwTransaction, Transaction, WriteFlags}; +use lmdb::{Cursor, Database, RwTransaction, Transaction, WriteFlags}; +use lmdb_sys::MDB_LAST; use serde::de::DeserializeOwned; #[cfg(test)] use serde::Serialize; @@ -410,6 +411,128 @@ pub(super) fn deserialize_bytesrepr(raw: &[u8]) -> Resul } } +/// Converts a `u64` into big-endian bytes suitable for use as an LMDB key where the natural +/// byte-lexicographic ordering of keys must match the numeric ordering of the values. +/// +/// `bytesrepr` encodes integers little-endian, which does not have this property, so keys for +/// indexes that need ordered ("get last") lookups (e.g. the tip of the block-height index) are +/// stored using this encoding instead of `serialize_bytesrepr`. +#[inline(always)] +fn be_key_bytes(key: u64) -> [u8; 8] { + key.to_be_bytes() +} + +/// Helper function to load a `bytesrepr`-serialized value keyed by a big-endian-encoded `u64`. +pub(super) fn get_by_be_u64_key( + txn: &Tx, + db: Database, + key: u64, +) -> Result, LmdbExtError> { + match txn.get(db, &be_key_bytes(key)) { + Ok(raw) => deserialize_bytesrepr(raw).map(Some), + Err(lmdb::Error::NotFound) => Ok(None), + Err(err) => Err(err.into()), + } +} + +/// Helper function to write a `bytesrepr`-serialized value keyed by a big-endian-encoded `u64`. +pub(super) fn put_by_be_u64_key( + txn: &mut RwTransaction, + db: Database, + key: u64, + value: &V, +) -> Result<(), LmdbExtError> { + let serialized_value = serialize_bytesrepr(value)?; + txn.put( + db, + &be_key_bytes(key), + &serialized_value, + WriteFlags::empty(), + )?; + Ok(()) +} + +/// Deletes the value keyed by a big-endian-encoded `u64`, tolerating a missing entry. +pub(super) fn delete_by_be_u64_key( + txn: &mut RwTransaction, + db: Database, + key: u64, +) -> Result<(), LmdbExtError> { + match txn.del(db, &be_key_bytes(key), None) { + Ok(()) | Err(lmdb::Error::NotFound) => Ok(()), + Err(err) => Err(err.into()), + } +} + +/// Deletes the value keyed by a `bytesrepr`-serialized key, tolerating a missing entry. +pub(super) fn delete_value_bytesrepr( + txn: &mut RwTransaction, + db: Database, + key: &K, +) -> Result<(), LmdbExtError> { + let serialized_key = serialize_bytesrepr(key)?; + match txn.del(db, &serialized_key, None) { + Ok(()) | Err(lmdb::Error::NotFound) => Ok(()), + Err(err) => Err(err.into()), + } +} + +/// Helper function to write a `bytesrepr`-serialized value keyed by a big-endian-encoded `u64`, +/// using LMDB's `APPEND` flag for fast bulk loading. +/// +/// Callers MUST insert keys in strictly increasing order (e.g. by iterating a `BTreeMap` in its +/// natural order) and the key must be greater than any key already in the database (e.g. the +/// database was just cleared) — otherwise LMDB returns an error rather than corrupting data. +pub(super) fn append_by_be_u64_key( + txn: &mut RwTransaction, + db: Database, + key: u64, + value: &V, +) -> Result<(), LmdbExtError> { + let serialized_value = serialize_bytesrepr(value)?; + txn.put( + db, + &be_key_bytes(key), + &serialized_value, + WriteFlags::APPEND, + )?; + Ok(()) +} + +/// Helper function to write a `bytesrepr`-serialized value keyed by a `bytesrepr`-serialized key, +/// using LMDB's `APPEND` flag for fast bulk loading. +/// +/// Callers MUST insert keys in strictly increasing order (by the key type's `Ord` impl, which +/// must agree with the byte-lexicographic order of its `bytesrepr` encoding — e.g. by iterating a +/// `BTreeMap` in its natural order) and the key must be greater than any key already in the +/// database (e.g. the database was just cleared) — otherwise LMDB returns an error rather than +/// corrupting data. +pub(super) fn append_value_bytesrepr( + txn: &mut RwTransaction, + db: Database, + key: &K, + value: &V, +) -> Result<(), LmdbExtError> { + let serialized_key = serialize_bytesrepr(key)?; + let serialized_value = serialize_bytesrepr(value)?; + txn.put(db, &serialized_key, &serialized_value, WriteFlags::APPEND)?; + Ok(()) +} + +/// Returns the `bytesrepr`-deserialized value associated with the highest big-endian-encoded +/// `u64` key in the database, if any (i.e. the equivalent of `BTreeMap::values().last()`). +pub(super) fn get_last_by_be_u64_key( + txn: &Tx, + db: Database, +) -> Result, LmdbExtError> { + let cursor = txn.open_ro_cursor(db)?; + match cursor.get(None, None, MDB_LAST) { + Ok((_, raw)) => deserialize_bytesrepr(raw).map(Some), + Err(lmdb::Error::NotFound) => Ok(None), + Err(err) => Err(err.into()), + } +} + /// Serializes into a buffer. #[inline(always)] pub(super) fn serialize_bytesrepr(value: &T) -> Result, LmdbExtError> { diff --git a/storage/src/block_store/lmdb/mod.rs b/storage/src/block_store/lmdb/mod.rs index 8c43d7b446..98892c5556 100644 --- a/storage/src/block_store/lmdb/mod.rs +++ b/storage/src/block_store/lmdb/mod.rs @@ -1,12 +1,9 @@ mod lmdb_ext; -mod temp_map; mod versioned_databases; -mod indexed_lmdb_block_store; mod lmdb_block_store; use core::convert::TryFrom; -pub use indexed_lmdb_block_store::IndexedLmdbBlockStore; pub use lmdb_block_store::LmdbBlockStore; #[cfg(test)] diff --git a/storage/src/block_store/lmdb/temp_map.rs b/storage/src/block_store/lmdb/temp_map.rs deleted file mode 100644 index 9e26afef9e..0000000000 --- a/storage/src/block_store/lmdb/temp_map.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::collections::BTreeMap; - -enum EntryState { - Deleted, - Occupied(V), -} - -/// A wrapper over a BTreeMap that stores changes to the backing map only temporarily. -/// The backing map will not be altered until the temporary changes are committed. -pub(crate) struct TempMap<'a, K, V: 'a> { - base_index: &'a mut BTreeMap, - new_index: BTreeMap>, -} - -impl<'a, K, V> TempMap<'a, K, V> -where - K: Ord, - V: 'a + Copy, -{ - /// Creates a new temporary map that is backed by a BTreeMap - pub(crate) fn new(base_index: &'a mut BTreeMap) -> Self { - Self { - base_index, - new_index: BTreeMap::>::new(), - } - } - - /// Reads the value contained in the map at the specified key. - pub(crate) fn get(&self, key: &K) -> Option { - if let Some(state) = self.new_index.get(key) { - match state { - EntryState::Occupied(val) => Some(*val), - EntryState::Deleted => None, - } - } else { - self.base_index.get(key).copied() - } - } - - /// Checks if a key exists in this map. - pub(crate) fn contains_key(&self, key: &K) -> bool { - if self.new_index.contains_key(key) { - true - } else { - self.base_index.contains_key(key) - } - } - - /// Sets the value at the specified key index. - pub(crate) fn insert(&mut self, key: K, val: V) { - self.new_index.insert(key, EntryState::Occupied(val)); - } - - /// Removes the value from the map. - pub(crate) fn remove(&mut self, key: K) { - if self.contains_key(&key) { - self.new_index.insert(key, EntryState::Deleted); - } - } - - /// Saves temporary changes to the backing map. - pub(crate) fn commit(self) { - for (key, val) in self.new_index { - match val { - EntryState::Occupied(val) => self.base_index.insert(key, val), - EntryState::Deleted => self.base_index.remove(&key), - }; - } - } -} diff --git a/storage/src/block_store/types/block_hash_height_and_era.rs b/storage/src/block_store/types/block_hash_height_and_era.rs index 8aa5e64431..2b6bd2b385 100644 --- a/storage/src/block_store/types/block_hash_height_and_era.rs +++ b/storage/src/block_store/types/block_hash_height_and_era.rs @@ -4,7 +4,10 @@ use rand::Rng; #[cfg(test)] use casper_types::testing::TestRng; -use casper_types::{BlockHash, BlockHashAndHeight, EraId}; +use casper_types::{ + bytesrepr::{self, FromBytes, ToBytes}, + BlockHash, BlockHashAndHeight, EraId, +}; /// Aggregates block identifying information. #[derive(Clone, Copy, Debug, DataSize)] @@ -43,3 +46,35 @@ impl From for BlockHashAndHeight { BlockHashAndHeight::new(bhhe.block_hash, bhhe.block_height) } } + +impl ToBytes for BlockHashHeightAndEra { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.extend(self.block_hash.to_bytes()?); + buffer.extend(self.block_height.to_bytes()?); + buffer.extend(self.era_id.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.block_hash.serialized_length() + + self.block_height.serialized_length() + + self.era_id.serialized_length() + } +} + +impl FromBytes for BlockHashHeightAndEra { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (block_hash, remainder) = BlockHash::from_bytes(bytes)?; + let (block_height, remainder) = u64::from_bytes(remainder)?; + let (era_id, remainder) = EraId::from_bytes(remainder)?; + Ok(( + BlockHashHeightAndEra { + block_hash, + block_height, + era_id, + }, + remainder, + )) + } +} diff --git a/storage/src/data_access_layer/protocol_upgrade.rs b/storage/src/data_access_layer/protocol_upgrade.rs index 691d06f720..f74c8c9530 100644 --- a/storage/src/data_access_layer/protocol_upgrade.rs +++ b/storage/src/data_access_layer/protocol_upgrade.rs @@ -3,7 +3,7 @@ use casper_types::{execution::Effects, Digest, ProtocolUpgradeConfig}; use crate::system::protocol_upgrade::ProtocolUpgradeError; /// Request to upgrade the protocol. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct ProtocolUpgradeRequest { config: ProtocolUpgradeConfig, }