Skip to content

Commit 4e3855d

Browse files
authored
Merge pull request #3030 from ljedrz/perf/cache_block_tree
[Perf] Facilitate the caching of the block tree
2 parents 227cd09 + e84a844 commit 4e3855d

File tree

10 files changed

+209
-57
lines changed

10 files changed

+209
-57
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

console/algorithms/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ version = "0.13"
5454
features = [ "digest", "ecdsa" ]
5555
default-features = false
5656

57+
[dependencies.serde]
58+
workspace = true
59+
features = ["rc"]
60+
5761
[dependencies.smallvec]
5862
workspace = true
5963
features = [ "const_generics", "const_new" ]
@@ -77,9 +81,6 @@ workspace = true
7781
[dev-dependencies.hex]
7882
workspace = true
7983

80-
[dev-dependencies.serde]
81-
workspace = true
82-
8384
[dev-dependencies.serde_json]
8485
workspace = true
8586
features = [ "preserve_order" ]

console/algorithms/src/bhp/hasher/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::Blake2Xs;
1919
use snarkvm_console_types::prelude::*;
2020
use snarkvm_utilities::BigInteger;
2121

22+
use serde::{Deserialize, Serialize};
2223
use std::sync::Arc;
2324

2425
/// The BHP chunk size (this implementation is for a 3-bit BHP).
@@ -27,7 +28,8 @@ pub(super) const BHP_LOOKUP_SIZE: usize = 1 << BHP_CHUNK_SIZE;
2728

2829
/// BHP is a collision-resistant hash function that takes a variable-length input.
2930
/// The BHP hasher is used to process one internal iteration of the BHP hash function.
30-
#[derive(Clone, Debug, PartialEq)]
31+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
32+
#[serde(bound = "E: Serialize + DeserializeOwned")]
3133
pub struct BHPHasher<E: Environment, const NUM_WINDOWS: u8, const WINDOW_SIZE: u8> {
3234
/// The bases for the BHP hash.
3335
bases: Arc<Vec<Vec<Group<E>>>>,

console/algorithms/src/bhp/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod hash_uncompressed;
2323

2424
use snarkvm_console_types::prelude::*;
2525

26+
use serde::{Deserialize, Serialize};
2627
use std::sync::Arc;
2728

2829
const BHP_CHUNK_SIZE: usize = 3;
@@ -50,7 +51,8 @@ pub type BHP1024<E> = BHP<E, 8, 54>; // Supports inputs up to 1044 bits (4 u8 +
5051
/// ```text
5152
/// DIGEST_N+1 = BHP([ DIGEST_N[0..DATA_BITS] || INPUT[(N+1)*BLOCK_SIZE..(N+2)*BLOCK_SIZE] ]);
5253
/// ```
53-
#[derive(Clone, Debug, PartialEq)]
54+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
55+
#[serde(bound = "E: Serialize + DeserializeOwned")]
5456
pub struct BHP<E: Environment, const NUM_WINDOWS: u8, const WINDOW_SIZE: u8> {
5557
/// The domain separator for the BHP hash function.
5658
domain: Vec<bool>,

console/collections/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ workspace = true
3131
[dependencies.rayon]
3232
workspace = true
3333

34+
[dependencies.serde]
35+
workspace = true
36+
3437
[dev-dependencies.snarkvm-console-network]
3538
path = "../network"
3639

console/collections/src/merkle_tree/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ use snarkvm_console_types::prelude::*;
2626

2727
use aleo_std::prelude::*;
2828

29+
use serde::{Deserialize, Serialize};
2930
use std::collections::BTreeMap;
3031

3132
#[cfg(not(feature = "serial"))]
3233
use rayon::prelude::*;
3334

34-
#[derive(Clone)]
35+
#[derive(Clone, Deserialize, Serialize)]
36+
#[serde(bound = "E: Serialize + DeserializeOwned, LH: Serialize + DeserializeOwned, PH: Serialize + DeserializeOwned")]
3537
pub struct MerkleTree<E: Environment, LH: LeafHash<Hash = PH::Hash>, PH: PathHash<Hash = Field<E>>, const DEPTH: u8> {
3638
/// The leaf hasher for the Merkle tree.
3739
leaf_hasher: LH,

ledger/src/lib.rs

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,24 @@ pub enum RecordsFilter<N: Network> {
114114
/// which loads the ledger from storage,
115115
/// or initializes it with the genesis block if the storage is empty
116116
#[derive(Clone)]
117-
pub struct Ledger<N: Network, C: ConsensusStorage<N>> {
117+
pub struct Ledger<N: Network, C: ConsensusStorage<N>>(Arc<InnerLedger<N, C>>);
118+
119+
impl<N: Network, C: ConsensusStorage<N>> Deref for Ledger<N, C> {
120+
type Target = InnerLedger<N, C>;
121+
122+
fn deref(&self) -> &Self::Target {
123+
&self.0
124+
}
125+
}
126+
127+
#[doc(hidden)]
128+
pub struct InnerLedger<N: Network, C: ConsensusStorage<N>> {
118129
/// The VM state.
119130
vm: VM<N, C>,
120131
/// The genesis block.
121132
genesis_block: Block<N>,
122133
/// The current epoch hash.
123-
current_epoch_hash: Arc<RwLock<Option<N::BlockHash>>>,
124-
134+
current_epoch_hash: RwLock<Option<N::BlockHash>>,
125135
/// The committee resulting from all the on-chain staking activity.
126136
///
127137
/// This includes any bonding and unbonding transactions in the latest block.
@@ -137,23 +147,21 @@ pub struct Ledger<N: Network, C: ConsensusStorage<N>> {
137147
/// So the `Option` should always be `Some`,
138148
/// but there are cases in which it is `None`,
139149
/// probably only temporarily when loading/initializing the ledger,
140-
current_committee: Arc<RwLock<Option<Committee<N>>>>,
150+
current_committee: RwLock<Option<Committee<N>>>,
141151

142152
/// The latest block that was added to the ledger.
143153
///
144154
/// This lock is also used as a way to prevent concurrent updates to the ledger, and to ensure that
145155
/// the ledger does not advance while certain check happen.
146-
current_block: Arc<RwLock<Block<N>>>,
147-
156+
current_block: RwLock<Block<N>>,
148157
/// The recent committees of interest paired with their applicable rounds.
149158
///
150159
/// Each entry consisting of a round `R` and a committee `C`,
151160
/// says that `C` is the bonded committee at round `R`,
152161
/// i.e. resulting from all the bonding and unbonding transactions before `R`.
153162
/// If `L` is the lookback round distance, `C` is the active committee at round `R + L`
154163
/// (i.e. the committee in charge of running consensus at round `R + L`).
155-
committee_cache: Arc<Mutex<LruCache<u64, Committee<N>>>>,
156-
164+
committee_cache: Mutex<LruCache<u64, Committee<N>>>,
157165
/// The cache that holds the provers and the number of solutions they have submitted for the current epoch.
158166
epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
159167
}
@@ -211,47 +219,73 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
211219
let current_committee = vm.finalize_store().committee_store().current_committee().ok();
212220

213221
// Create a committee cache.
214-
let committee_cache = Arc::new(Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap())));
222+
let committee_cache = Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap()));
215223

216224
// Initialize the ledger.
217-
let mut ledger = Self {
225+
let ledger = Self(Arc::new(InnerLedger {
218226
vm,
219227
genesis_block: genesis_block.clone(),
220228
current_epoch_hash: Default::default(),
221-
current_committee: Arc::new(RwLock::new(current_committee)),
222-
current_block: Arc::new(RwLock::new(genesis_block.clone())),
229+
current_committee: RwLock::new(current_committee),
230+
current_block: RwLock::new(genesis_block.clone()),
223231
committee_cache,
224232
epoch_provers_cache: Default::default(),
225-
};
233+
}));
234+
235+
// Attempt to obtain the maximum height from the storage.
236+
let max_stored_height = ledger.vm.block_store().max_height();
226237

227238
// If the block store is empty, add the genesis block.
228-
if ledger.vm.block_store().max_height().is_none() {
229-
// Add the genesis block.
239+
let latest_height = if let Some(max_height) = max_stored_height {
240+
max_height
241+
} else {
230242
ledger.advance_to_next_block(&genesis_block)?;
231-
}
243+
0
244+
};
232245
lap!(timer, "Initialize genesis");
233246

234-
// Retrieve the latest height.
235-
let latest_height =
236-
ledger.vm.block_store().max_height().with_context(|| "Failed to load blocks from the ledger")?;
247+
// Ensure that the greatest stored height matches that of the block tree.
248+
ensure!(
249+
latest_height == ledger.vm().block_store().current_block_height(),
250+
"The stored height is different than the one in the block tree; \
251+
please ensure that the cached block tree is valid or delete the \
252+
'block_tree' file from the ledger folder"
253+
);
254+
255+
// Verify that the root of the cached block tree matches the one in the storage.
256+
let tree_root = <N::StateRoot>::from(ledger.vm().block_store().get_block_tree_root());
257+
let state_root = ledger
258+
.vm()
259+
.block_store()
260+
.get_state_root(latest_height)?
261+
.ok_or_else(|| anyhow!("Missing state root in the storage"))?;
262+
ensure!(
263+
tree_root == state_root,
264+
"The stored state root is different than the one in the block tree;
265+
please ensure that the cached block tree is valid or delete the \
266+
'block_tree' file from the ledger folder"
267+
);
268+
237269
// Fetch the latest block.
238270
let block = ledger
239271
.get_block(latest_height)
240272
.with_context(|| format!("Failed to load block {latest_height} from the ledger"))?;
241273

242274
// Set the current block.
243-
ledger.current_block = Arc::new(RwLock::new(block));
275+
*ledger.current_block.write() = block;
244276
// Set the current committee (and ensures the latest committee exists).
245-
ledger.current_committee = Arc::new(RwLock::new(Some(ledger.latest_committee()?)));
277+
*ledger.current_committee.write() = Some(ledger.latest_committee()?);
246278
// Set the current epoch hash.
247-
ledger.current_epoch_hash = Arc::new(RwLock::new(Some(ledger.get_epoch_hash(latest_height)?)));
279+
*ledger.current_epoch_hash.write() = Some(ledger.get_epoch_hash(latest_height)?);
248280
// Set the epoch prover cache.
249-
ledger.epoch_provers_cache = Arc::new(RwLock::new(ledger.load_epoch_provers()));
281+
*ledger.epoch_provers_cache.write() = ledger.load_epoch_provers();
250282

251283
finish!(timer, "Initialize ledger");
252284
Ok(ledger)
253285
}
286+
}
254287

288+
impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
255289
/// Creates a rocksdb checkpoint in the specified directory, which needs to not exist at the
256290
/// moment of calling. The checkpoints are based on hard links, which means they can both be
257291
/// incremental (i.e. they aren't full physical copies), and used as full rollback points
@@ -261,6 +295,11 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
261295
self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
262296
}
263297

298+
#[cfg(feature = "rocks")]
299+
pub fn cache_block_tree(&self) -> Result<()> {
300+
self.vm.block_store().cache_block_tree()
301+
}
302+
264303
/// Loads the provers and the number of solutions they have submitted for the current epoch.
265304
pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
266305
// Fetch the block heights that belong to the current epoch.
@@ -285,12 +324,12 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
285324
}
286325

287326
/// Returns the VM.
288-
pub const fn vm(&self) -> &VM<N, C> {
327+
pub fn vm(&self) -> &VM<N, C> {
289328
&self.vm
290329
}
291330

292331
/// Returns the puzzle.
293-
pub const fn puzzle(&self) -> &Puzzle<N> {
332+
pub fn puzzle(&self) -> &Puzzle<N> {
294333
self.vm.puzzle()
295334
}
296335

@@ -481,6 +520,20 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
481520
}
482521
}
483522

523+
#[cfg(feature = "rocks")]
524+
impl<N: Network, C: ConsensusStorage<N>> Drop for InnerLedger<N, C> {
525+
fn drop(&mut self) {
526+
// Cache the block tree in order to speed up the next startup; this operation
527+
// is guaranteed to conclude as long as the destructors are allowed to run
528+
// (a clean shutdown, panic = "unwind", an explicit call to `drop`, etc.).
529+
// At the moment this code is executed, the Ledger is guaranteed to be owned
530+
// exclusively by this method, so no other activity may interrupt it.
531+
if let Err(e) = self.vm.block_store().cache_block_tree() {
532+
error!("Couldn't cache the block tree: {e}");
533+
}
534+
}
535+
}
536+
484537
pub mod prelude {
485538
pub use crate::{Ledger, authority, block, block::*, committee, helpers::*, narwhal, puzzle, query, store};
486539
}

ledger/store/src/block/mod.rs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ use snarkvm_ledger_puzzle::{Solution, SolutionID};
4848
use snarkvm_synthesizer_program::{FinalizeOperation, Program};
4949

5050
use aleo_std_storage::StorageMode;
51+
#[cfg(feature = "rocks")]
52+
use aleo_std_storage::aleo_ledger_dir;
5153
use anyhow::{Context, Result};
5254
#[cfg(feature = "locktick")]
5355
use locktick::{LockGuard, parking_lot::RwLock};
5456
#[cfg(not(feature = "locktick"))]
5557
use parking_lot::RwLock;
5658
use std::{borrow::Cow, sync::Arc};
57-
use tracing::debug;
59+
#[cfg(feature = "rocks")]
60+
use std::{fs, io::BufWriter};
5861

5962
#[cfg(not(feature = "serial"))]
6063
use rayon::prelude::*;
@@ -991,6 +994,8 @@ pub trait BlockStorage<N: Network>: 'static + Clone + Send + Sync {
991994

992995
#[cfg(feature = "rocks")]
993996
fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), String>;
997+
998+
fn create_block_tree(&self) -> Result<BlockTree<N>>;
994999
}
9951000

9961001
/// The `BlockStore` is the user facing API that either uses `BlockMemory` or `BlockDB` as its storae backend.
@@ -1009,17 +1014,7 @@ impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {
10091014
pub fn open<S: Into<StorageMode>>(storage: S) -> Result<Self> {
10101015
let storage = B::open(storage)?;
10111016

1012-
// Prepare an iterator over the block heights and prepare the leaves of the block tree.
1013-
let hashes = storage
1014-
.id_map()
1015-
.iter_confirmed()
1016-
.sorted_unstable_by(|(h1, _), (h2, _)| h1.cmp(h2))
1017-
.map(|(_, hash)| hash.to_bits_le())
1018-
.collect::<Vec<Vec<bool>>>();
1019-
1020-
// Construct the block tree.
1021-
debug!("Found {} blocks on disk", hashes.len());
1022-
let tree = N::merkle_tree_bhp(&hashes)?;
1017+
let tree = storage.create_block_tree()?;
10231018

10241019
let mut initial_cache = Vec::new();
10251020
let cache_end_height = u32::try_from(tree.number_of_leaves())?;
@@ -1032,10 +1027,12 @@ impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {
10321027
}
10331028

10341029
// Get the hash for the next block to add to the cache.
1035-
let hash = storage
1036-
.id_map()
1037-
.get_confirmed(&height)?
1038-
.with_context(|| format!("Block {height} exists in tree but not in storage"))?;
1030+
let hash = storage.id_map().get_confirmed(&height)?.with_context(|| {
1031+
format!(
1032+
"Block {height} exists in tree but not in storage;\
1033+
perhaps you used a wrong block tree cache file?"
1034+
)
1035+
})?;
10391036

10401037
initial_cache.push(
10411038
storage.get_block(&hash)?.with_context(|| format!("Block {hash} exists in tree but not in storage"))?,
@@ -1200,10 +1197,40 @@ impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {
12001197
self.storage.unpause_atomic_writes::<DISCARD_BATCH>()
12011198
}
12021199

1200+
/// Stores a database backup at the given location.
12031201
#[cfg(feature = "rocks")]
12041202
pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), String> {
12051203
self.storage.backup_database(path)
12061204
}
1205+
1206+
/// Serializes and persists the current block tree.
1207+
#[cfg(feature = "rocks")]
1208+
pub fn cache_block_tree(&self) -> Result<()> {
1209+
// Prepare the path for the target file.
1210+
let mut path = aleo_ledger_dir(N::ID, self.storage.storage_mode());
1211+
path.push("block_tree");
1212+
1213+
// Create the target file.
1214+
let file = fs::File::create(path)?;
1215+
// The block tree can become quite large, so use a BufWriter in order to
1216+
// not have to keep the entire serialized tree in memory, and to reduce
1217+
// the number of syscalls involved with disk writes. 1MiB should provide
1218+
// a good balance between the CPU cache and maximum disk throughput.
1219+
let mut writer = BufWriter::with_capacity(1024 * 1024, file);
1220+
bincode::serialize_into(&mut writer, &&*self.tree.read())?;
1221+
writer.flush()?;
1222+
// TODO(ljedrz): this operation can already take ~2.5s, so we may want
1223+
// to perform chunking and parallel serialization. This may be useful
1224+
// for other applications, so it should be implemented as a common
1225+
// utility.
1226+
1227+
Ok(())
1228+
}
1229+
1230+
/// Returns the root of the block tree.
1231+
pub fn get_block_tree_root(&self) -> Field<N> {
1232+
*self.tree.read().root()
1233+
}
12071234
}
12081235

12091236
impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {

0 commit comments

Comments
 (0)