1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 sync::{Arc, Mutex},
5};
6
7use arc_swap::ArcSwap;
8use fjall::{KeyspaceCreateOptions, PersistMode, Readable};
9use monero_oxide::transaction::Transaction;
10use tapes::{Persistence, TapeOpenOptions, Tapes, TapesRead, TapesReadTransaction};
11
12use cuprate_helper::cast::u64_to_usize;
13
14use crate::{
15 config::Config,
16 types::{Amount, BlockInfo, RctOutput, TxInfo},
17 BlockchainError,
18};
19
20pub(crate) const CHAIN_TIP_KEY: &[u8] = b"tip";
22
23fn recreate_fjall_keyspace(
25 database: &fjall::Database,
26 keyspace: &fjall::Keyspace,
27) -> Result<fjall::Keyspace, BlockchainError> {
28 let name = keyspace.name().to_string();
29
30 database.delete_keyspace(keyspace.clone())?;
31 Ok(database.keyspace(&name, KeyspaceCreateOptions::default)?)
32}
33
34pub(crate) fn reset_fjall_keyspace(
36 database: &fjall::Database,
37 keyspace: &ArcSwap<fjall::Keyspace>,
38) -> Result<(), BlockchainError> {
39 let new_keyspace = recreate_fjall_keyspace(database, &keyspace.load())?;
40 keyspace.store(Arc::new(new_keyspace));
41
42 Ok(())
43}
44
45pub struct BlockchainDatabase {
47 pub(crate) config: Config,
49
50 pub(crate) linear_tapes: Tapes,
52 pub(crate) fjall: fjall::Database,
54
55 pub(crate) block_heights: fjall::Keyspace,
61 pub(crate) chain_tip: fjall::Keyspace,
67 pub(crate) key_images: fjall::Keyspace,
73 pub(crate) pre_rct_outputs: fjall::Keyspace,
79 pub(crate) tx_ids: fjall::Keyspace,
85 pub(crate) v1_tx_outputs: fjall::Keyspace,
91 pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
97 pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
103 pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
109 pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
115 pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
121 pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
127
128 pub(crate) rct_outputs: tapes::FixedSizedTape<RctOutput>,
134 pub(crate) tx_infos: tapes::FixedSizedTape<TxInfo>,
140 pub(crate) block_infos: tapes::FixedSizedTape<BlockInfo>,
146 pub(crate) pruned_blobs: tapes::BlobTape,
162 pub(crate) v1_prunable_blobs: tapes::BlobTape,
166 pub(crate) prunable_blobs: Vec<tapes::BlobTape>,
172
173 pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
176}
177
178impl BlockchainDatabase {
179 pub fn open_with_fjall_database(
181 config: &Config,
182 fjall: fjall::Database,
183 ) -> Result<Self, BlockchainError> {
184 let block_heights = fjall.keyspace("block_heights", KeyspaceCreateOptions::default)?;
185 let chain_tip = fjall.keyspace("chain_tip", KeyspaceCreateOptions::default)?;
186 let key_images = fjall.keyspace("key_images", KeyspaceCreateOptions::default)?;
187 let pre_rct_outputs = fjall.keyspace("pre_rct_outputs", KeyspaceCreateOptions::default)?;
188 let tx_ids = fjall.keyspace("tx_ids", KeyspaceCreateOptions::default)?;
189 let v1_tx_outputs = fjall.keyspace("tx_outputs", KeyspaceCreateOptions::default)?;
190
191 let alt_chain_infos = fjall.keyspace("alt_chain_infos", KeyspaceCreateOptions::default)?;
192 let alt_block_heights =
193 fjall.keyspace("alt_block_heights", KeyspaceCreateOptions::default)?;
194 let alt_block_infos = fjall.keyspace("alt_block_infos", KeyspaceCreateOptions::default)?;
195 let alt_block_blobs = fjall.keyspace("alt_block_blobs", KeyspaceCreateOptions::default)?;
196 let alt_transaction_blobs =
197 fjall.keyspace("alt_transaction_blobs", KeyspaceCreateOptions::default)?;
198 let alt_transaction_infos =
199 fjall.keyspace("alt_transaction_infos", KeyspaceCreateOptions::default)?;
200
201 let tapes_index_dir = config.index_dir.join("tapes");
202 let tapes_blob_dir = config.blob_dir.join("tapes");
203
204 let linear_tapes = Tapes::open(&tapes_index_dir)?;
205 let mut tape_append_tx = linear_tapes.append();
206
207 let rct_outputs = tape_append_tx.open_fixed_sized_tape(
208 "rct_outputs",
209 &TapeOpenOptions {
210 top_cache_size: config.cache_sizes.rct_outputs,
211 dir: tapes_index_dir.clone(),
212 },
213 )?;
214 let tx_infos = tape_append_tx.open_fixed_sized_tape(
215 "tx_infos",
216 &TapeOpenOptions {
217 top_cache_size: config.cache_sizes.tx_infos,
218 dir: tapes_index_dir.clone(),
219 },
220 )?;
221 let block_infos = tape_append_tx.open_fixed_sized_tape(
222 "block_infos",
223 &TapeOpenOptions {
224 top_cache_size: config.cache_sizes.block_infos,
225 dir: tapes_index_dir,
226 },
227 )?;
228 let pruned_blobs = tape_append_tx.open_blob_tape(
229 "pruned_blobs",
230 &TapeOpenOptions {
231 top_cache_size: config.cache_sizes.pruned_blobs,
232 dir: tapes_blob_dir.clone(),
233 },
234 )?;
235 let v1_prunable_blobs = tape_append_tx.open_blob_tape(
236 "v1_prunable_blobs",
237 &TapeOpenOptions {
238 top_cache_size: config.cache_sizes.v1_prunable_blobs,
239 dir: tapes_blob_dir.clone(),
240 },
241 )?;
242
243 const PRUNABLE_BLOBS: [&str; 8] = [
244 "prunable1",
245 "prunable2",
246 "prunable3",
247 "prunable4",
248 "prunable5",
249 "prunable6",
250 "prunable7",
251 "prunable8",
252 ];
253
254 let prunable_blobs = (0..8)
255 .map(|i| {
256 tape_append_tx.open_blob_tape(
257 PRUNABLE_BLOBS[i],
258 &TapeOpenOptions {
259 top_cache_size: config.cache_sizes.prunable_blobs,
260 dir: tapes_blob_dir.clone(),
261 },
262 )
263 })
264 .collect::<Result<_, _>>()?;
265
266 tape_append_tx.commit(Persistence::SyncAll)?;
267
268 tracing::debug!("opened db");
269 Ok(Self {
270 fjall,
271 linear_tapes,
272 config: config.clone(),
273 block_heights,
274 chain_tip,
275 key_images,
276 pre_rct_outputs,
277 tx_ids,
278 v1_tx_outputs,
279 alt_chain_infos: ArcSwap::from_pointee(alt_chain_infos),
280 alt_block_heights: ArcSwap::from_pointee(alt_block_heights),
281 alt_block_infos: ArcSwap::from_pointee(alt_block_infos),
282 alt_block_blobs: ArcSwap::from_pointee(alt_block_blobs),
283 alt_transaction_blobs: ArcSwap::from_pointee(alt_transaction_blobs),
284 alt_transaction_infos: ArcSwap::from_pointee(alt_transaction_infos),
285 rct_outputs,
286 tx_infos,
287 block_infos,
288 pruned_blobs,
289 v1_prunable_blobs,
290 prunable_blobs,
291 pre_rct_numb_outputs_cache: Mutex::new(HashMap::new()),
292 })
293 }
294
295 fn tips_match(
297 &self,
298 fjall: &impl Readable,
299 tapes: &impl TapesRead,
300 ) -> Result<bool, BlockchainError> {
301 let tapes_height = tapes
302 .fixed_sized_tape_len(&self.block_infos)
303 .expect("block_infos tape exists");
304 let tapes_tip = match tapes_height.checked_sub(1) {
305 Some(top_height) => Some(
306 tapes
307 .read_entry(&self.block_infos, top_height)?
308 .ok_or(BlockchainError::NotFound)?
309 .block_hash,
310 ),
311 None => None,
312 };
313 let fjall_tip = fjall.get(&self.chain_tip, CHAIN_TIP_KEY)?;
314
315 Ok(match (tapes_tip, fjall_tip.as_deref()) {
316 (None, None) => true,
317 (Some(tapes_tip), Some(fjall_tip)) => tapes_tip.as_slice() == fjall_tip,
318 _ => false,
319 })
320 }
321
322 pub fn read_transactions(
324 &self,
325 ) -> Result<(fjall::Snapshot, TapesReadTransaction<'_>), BlockchainError> {
326 loop {
327 let fjall = self.fjall.snapshot();
328 let tapes = self.linear_tapes.reader();
329
330 if self.tips_match(&fjall, &tapes)? {
331 return Ok((fjall, tapes));
332 }
333
334 }
336 }
337
338 pub fn make_consistent(&mut self) -> Result<(), BlockchainError> {
341 tracing::info!("Checking blockchain database consistency.");
342
343 let tips_match = {
344 let fjall = self.fjall.snapshot();
345 let tapes = self.linear_tapes.reader();
346 self.tips_match(&fjall, &tapes)?
347 };
348
349 if !tips_match {
350 tracing::warn!("fjall and tapes are out of sync");
351 self.rebuild_fjall_database()?;
352 }
353
354 Ok(())
355 }
356
357 pub fn rebuild_fjall_database(&mut self) -> Result<(), BlockchainError> {
359 self.block_heights = recreate_fjall_keyspace(&self.fjall, &self.block_heights)?;
360 self.chain_tip = recreate_fjall_keyspace(&self.fjall, &self.chain_tip)?;
361 self.key_images = recreate_fjall_keyspace(&self.fjall, &self.key_images)?;
362 self.pre_rct_outputs = recreate_fjall_keyspace(&self.fjall, &self.pre_rct_outputs)?;
363 self.tx_ids = recreate_fjall_keyspace(&self.fjall, &self.tx_ids)?;
364 self.v1_tx_outputs = recreate_fjall_keyspace(&self.fjall, &self.v1_tx_outputs)?;
365 reset_fjall_keyspace(&self.fjall, &self.alt_chain_infos)?;
366 reset_fjall_keyspace(&self.fjall, &self.alt_block_heights)?;
367 reset_fjall_keyspace(&self.fjall, &self.alt_block_infos)?;
368 reset_fjall_keyspace(&self.fjall, &self.alt_block_blobs)?;
369 reset_fjall_keyspace(&self.fjall, &self.alt_transaction_blobs)?;
370 reset_fjall_keyspace(&self.fjall, &self.alt_transaction_infos)?;
371
372 let rebuild_span = tracing::info_span!("rebuild_fjall_database");
373 let _guard = rebuild_span.enter();
374
375 tracing::info!("rebuilding fjall db");
376
377 let tapes_reader = self.linear_tapes.reader();
378
379 let tx_infos_iter = tapes_reader.iter_from(&self.tx_infos, 0)?;
380 let mut tx_iter = tx_infos_iter.map(|tx_info| {
381 let tx_info = tx_info.unwrap();
382
383 let mut tx_blob = vec![0; tx_info.pruned_size];
384 tapes_reader
385 .read_bytes(&self.pruned_blobs, tx_info.pruned_blob_idx, &mut tx_blob)
386 .unwrap();
387
388 let tx = Transaction::read(&mut tx_blob.as_slice()).unwrap();
389
390 Cow::Owned(tx)
391 });
392
393 let mut batch = self.fjall.batch().durability(Some(PersistMode::Buffer));
394 let mut numb_txs = 0;
395 for height in 0..tapes_reader
396 .fixed_sized_tape_len(&self.block_infos)
397 .expect("block_infos tape exists")
398 {
399 let block =
400 crate::ops::block::get_block(&u64_to_usize(height), None, &tapes_reader, self)?;
401
402 let _miner_tx = tx_iter.next();
403
404 crate::ops::block::add_block_to_dynamic_tables(
405 self,
406 &block,
407 &block.hash(),
408 &mut tx_iter,
409 &mut numb_txs,
410 &mut batch,
411 &mut self.pre_rct_numb_outputs_cache.lock().unwrap(),
412 )?;
413
414 if height % 1000 == 0 {
415 tracing::info!("{} blocks processed", height);
416 let old_batch = std::mem::replace(
417 &mut batch,
418 self.fjall.batch().durability(Some(PersistMode::Buffer)),
419 );
420
421 old_batch.commit()?;
422 }
423 }
424
425 batch.commit()?;
426
427 Ok(())
428 }
429}
430
431impl Drop for BlockchainDatabase {
432 fn drop(&mut self) {
433 tracing::info!(parent: &tracing::Span::none(), "Syncing blockchain database to storage.");
434
435 let _ = self.fjall.persist(PersistMode::SyncAll);
436
437 let _ = self.linear_tapes.append().commit(Persistence::SyncAll);
438 }
439}