1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 mem,
5 sync::{Arc, Mutex},
6};
7
8use arc_swap::ArcSwap;
9use fjall::{KeyspaceCreateOptions, PersistMode, Readable};
10use monero_oxide::transaction::Transaction;
11use rand::Rng;
12use tapes::{
13 CachedBlobTape, CachedTapeOpenOptions, FixedSizedTape, Persistence, RollingBlobTape,
14 RollingTapeOpenOptions, Tapes, TapesAppend, TapesAppendTransaction, TapesRead,
15 TapesReadTransaction, WholeBlobTape, WholeTapeOpenOptions,
16};
17
18use cuprate_helper::cast::{u32_to_usize, u64_to_usize, usize_to_u64};
19use cuprate_pruning::{PruningSeed, CRYPTONOTE_PRUNING_LOG_STRIPES, CRYPTONOTE_PRUNING_TIP_BLOCKS};
20
21use crate::{
22 config::Config,
23 types::{Amount, BlockInfo, RctOutput, TxInfo},
24 BlockchainError,
25};
26
27pub(crate) const CHAIN_TIP_KEY: &[u8] = b"tip";
29
30const PRUNABLE_BLOBS: [&str; 8] = [
31 "prunable1",
32 "prunable2",
33 "prunable3",
34 "prunable4",
35 "prunable5",
36 "prunable6",
37 "prunable7",
38 "prunable8",
39];
40
41const PRUNABLE_TIP_FILE_SIZE: u64 = 4 * 1024 * 1024;
42const PRUNABLE_BLOB_FILE_SIZE: u64 = 128 * 1024 * 1024;
43
44fn recreate_fjall_keyspace(
46 database: &fjall::Database,
47 keyspace: &fjall::Keyspace,
48) -> Result<fjall::Keyspace, BlockchainError> {
49 let name = keyspace.name().to_string();
50
51 database.delete_keyspace(keyspace.clone())?;
52 Ok(database.keyspace(&name, KeyspaceCreateOptions::default)?)
53}
54
55pub(crate) fn reset_fjall_keyspace(
57 database: &fjall::Database,
58 keyspace: &ArcSwap<fjall::Keyspace>,
59) -> Result<(), BlockchainError> {
60 let new_keyspace = recreate_fjall_keyspace(database, &keyspace.load())?;
61 keyspace.store(Arc::new(new_keyspace));
62
63 Ok(())
64}
65
66pub struct BlockchainDatabase {
68 pub(crate) config: Config,
70
71 pub(crate) linear_tapes: Tapes,
73 pub(crate) fjall: fjall::Database,
75
76 pub(crate) block_heights: fjall::Keyspace,
82 pub(crate) chain_tip: fjall::Keyspace,
88 pub(crate) key_images: fjall::Keyspace,
94 pub(crate) pre_rct_outputs: fjall::Keyspace,
100 pub(crate) tx_ids: fjall::Keyspace,
106 pub(crate) v1_tx_outputs: fjall::Keyspace,
112 pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
118 pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
124 pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
130 pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
136 pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
142 pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
148
149 pub(crate) rct_outputs: FixedSizedTape<RctOutput, CachedBlobTape<WholeBlobTape>>,
155 pub(crate) tx_infos: FixedSizedTape<TxInfo, CachedBlobTape<WholeBlobTape>>,
161 pub(crate) block_infos: FixedSizedTape<BlockInfo, CachedBlobTape<WholeBlobTape>>,
167 pub(crate) pruned_blobs: CachedBlobTape<WholeBlobTape>,
183 pub(crate) v1_prunable_blobs: CachedBlobTape<WholeBlobTape>,
187 pub(crate) prunable_tables: PrunableTables,
189
190 pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
193}
194
195pub(crate) enum PrunableTables {
197 Full(Vec<CachedBlobTape<WholeBlobTape>>),
202 Pruned {
204 stripe: u32,
206 kept_stripe: CachedBlobTape<WholeBlobTape>,
208 prunable_tip: FixedSizedTape<u64, CachedBlobTape<RollingBlobTape>>,
219 prunable_tip_blobs: CachedBlobTape<RollingBlobTape>,
221 },
222}
223
224impl PrunableTables {
225 pub(crate) fn try_get_prunable_tape(
228 &self,
229 stripe: u32,
230 ) -> Option<&CachedBlobTape<WholeBlobTape>> {
231 match self {
232 Self::Full(vec) => vec.get(stripe as usize - 1),
233 Self::Pruned {
234 stripe: our_stripe,
235 kept_stripe,
236 ..
237 } if *our_stripe == stripe => Some(kept_stripe),
238 Self::Pruned { .. } => None,
239 }
240 }
241
242 fn open(
244 tape_append_tx: &mut TapesAppendTransaction,
245 config: &Config,
246 ) -> Result<Self, BlockchainError> {
247 let is_pruned = tape_append_tx.tape_exists("prunable_tip");
249
250 let prunable_tape_open_options = CachedTapeOpenOptions {
251 top_cache_size: config.cache_sizes.prunable_blobs,
252 inner: WholeTapeOpenOptions {
253 dir: config.blob_dir.clone(),
254 },
255 };
256
257 if is_pruned {
258 let kept_stripe = (0..8).find_map(|i| {
262 if tape_append_tx.tape_exists(PRUNABLE_BLOBS[i]) {
263 Some((
264 i + 1,
265 tape_append_tx
266 .open_blob_tape(PRUNABLE_BLOBS[i], prunable_tape_open_options.clone()),
267 ))
268 } else {
269 None
270 }
271 });
272
273 let Some((stripe, kept_stripe)) = kept_stripe else {
274 return Err(BlockchainError::NotFound);
275 };
276 let kept_stripe = kept_stripe?;
277
278 let prunable_tip_blobs = tape_append_tx.open_blob_tape(
282 "prunable_tip_blobs",
283 CachedTapeOpenOptions {
284 inner: RollingTapeOpenOptions {
285 file_size: PRUNABLE_BLOB_FILE_SIZE,
286 dir: config.blob_dir.clone(),
287 start_index: 0,
288 },
289 top_cache_size: config.cache_sizes.prunable_blobs,
290 },
291 )?;
292
293 let prunable_tip = tape_append_tx.open_fixed_sized_tape(
294 "prunable_tip",
295 CachedTapeOpenOptions {
296 inner: RollingTapeOpenOptions {
297 file_size: PRUNABLE_TIP_FILE_SIZE,
298 dir: config.index_dir.clone(),
299 start_index: 0,
300 },
301 top_cache_size: config.cache_sizes.prunable_tip,
302 },
303 )?;
304
305 Ok(Self::Pruned {
306 stripe: stripe
307 .try_into()
308 .expect("Pruning stripe is in range as we just created it"),
309 kept_stripe,
310 prunable_tip,
311 prunable_tip_blobs,
312 })
313 } else {
314 Ok(Self::Full(
315 (0..8)
316 .map(|i| {
317 tape_append_tx
318 .open_blob_tape(PRUNABLE_BLOBS[i], prunable_tape_open_options.clone())
319 })
320 .collect::<Result<Vec<_>, _>>()?,
321 ))
322 }
323 }
324}
325
326impl BlockchainDatabase {
327 pub fn open_with_fjall_database(
329 config: &Config,
330 fjall: fjall::Database,
331 ) -> Result<Self, BlockchainError> {
332 let block_heights = fjall.keyspace("block_heights", KeyspaceCreateOptions::default)?;
333 let chain_tip = fjall.keyspace("chain_tip", KeyspaceCreateOptions::default)?;
334 let key_images = fjall.keyspace("key_images", KeyspaceCreateOptions::default)?;
335 let pre_rct_outputs = fjall.keyspace("pre_rct_outputs", KeyspaceCreateOptions::default)?;
336 let tx_ids = fjall.keyspace("tx_ids", KeyspaceCreateOptions::default)?;
337 let v1_tx_outputs = fjall.keyspace("tx_outputs", KeyspaceCreateOptions::default)?;
338
339 let alt_chain_infos = fjall.keyspace("alt_chain_infos", KeyspaceCreateOptions::default)?;
340 let alt_block_heights =
341 fjall.keyspace("alt_block_heights", KeyspaceCreateOptions::default)?;
342 let alt_block_infos = fjall.keyspace("alt_block_infos", KeyspaceCreateOptions::default)?;
343 let alt_block_blobs = fjall.keyspace("alt_block_blobs", KeyspaceCreateOptions::default)?;
344 let alt_transaction_blobs =
345 fjall.keyspace("alt_transaction_blobs", KeyspaceCreateOptions::default)?;
346 let alt_transaction_infos =
347 fjall.keyspace("alt_transaction_infos", KeyspaceCreateOptions::default)?;
348
349 let linear_tapes = Tapes::open(&config.index_dir)?;
350 let mut tape_append_tx = linear_tapes.append();
351
352 let rct_outputs = tape_append_tx.open_fixed_sized_tape(
353 "rct_outputs",
354 CachedTapeOpenOptions {
355 top_cache_size: config.cache_sizes.rct_outputs,
356 inner: WholeTapeOpenOptions {
357 dir: config.index_dir.clone(),
358 },
359 },
360 )?;
361 let tx_infos = tape_append_tx.open_fixed_sized_tape(
362 "tx_infos",
363 CachedTapeOpenOptions {
364 top_cache_size: config.cache_sizes.tx_infos,
365 inner: WholeTapeOpenOptions {
366 dir: config.index_dir.clone(),
367 },
368 },
369 )?;
370 let block_infos = tape_append_tx.open_fixed_sized_tape(
371 "block_infos",
372 CachedTapeOpenOptions {
373 top_cache_size: config.cache_sizes.block_infos,
374 inner: WholeTapeOpenOptions {
375 dir: config.index_dir.clone(),
376 },
377 },
378 )?;
379
380 let pruned_blobs = tape_append_tx.open_blob_tape(
381 "pruned_blobs",
382 CachedTapeOpenOptions {
383 top_cache_size: config.cache_sizes.pruned_blobs,
384 inner: WholeTapeOpenOptions {
385 dir: config.blob_dir.clone(),
386 },
387 },
388 )?;
389 let v1_prunable_blobs = tape_append_tx.open_blob_tape(
390 "v1_prunable_blobs",
391 CachedTapeOpenOptions {
392 top_cache_size: config.cache_sizes.v1_prunable_blobs,
393 inner: WholeTapeOpenOptions {
394 dir: config.blob_dir.clone(),
395 },
396 },
397 )?;
398
399 let prunable_tables = PrunableTables::open(&mut tape_append_tx, config)?;
400
401 tape_append_tx.commit(Persistence::SyncAll)?;
402
403 tracing::debug!("opened db");
404 Ok(Self {
405 fjall,
406 linear_tapes,
407 config: config.clone(),
408 block_heights,
409 chain_tip,
410 key_images,
411 pre_rct_outputs,
412 tx_ids,
413 v1_tx_outputs,
414 alt_chain_infos: ArcSwap::from_pointee(alt_chain_infos),
415 alt_block_heights: ArcSwap::from_pointee(alt_block_heights),
416 alt_block_infos: ArcSwap::from_pointee(alt_block_infos),
417 alt_block_blobs: ArcSwap::from_pointee(alt_block_blobs),
418 alt_transaction_blobs: ArcSwap::from_pointee(alt_transaction_blobs),
419 alt_transaction_infos: ArcSwap::from_pointee(alt_transaction_infos),
420 rct_outputs,
421 tx_infos,
422 block_infos,
423 pruned_blobs,
424 v1_prunable_blobs,
425 prunable_tables,
426 pre_rct_numb_outputs_cache: Mutex::new(HashMap::new()),
427 })
428 }
429
430 fn tips_match(
432 &self,
433 fjall: &impl Readable,
434 tapes: &impl TapesRead,
435 ) -> Result<bool, BlockchainError> {
436 let tapes_height = tapes
437 .fixed_sized_tape_len(&self.block_infos)
438 .expect("block_infos tape exists");
439 let tapes_tip = match tapes_height.checked_sub(1) {
440 Some(top_height) => Some(
441 tapes
442 .read_entry(&self.block_infos, top_height)?
443 .ok_or(BlockchainError::NotFound)?
444 .block_hash,
445 ),
446 None => None,
447 };
448 let fjall_tip = fjall.get(&self.chain_tip, CHAIN_TIP_KEY)?;
449
450 Ok(match (tapes_tip, fjall_tip.as_deref()) {
451 (None, None) => fjall.is_empty(&self.block_heights)?,
455 (Some(tapes_tip), Some(fjall_tip)) => tapes_tip.as_slice() == fjall_tip,
456 _ => false,
457 })
458 }
459
460 pub fn read_transactions(
462 &self,
463 ) -> Result<(fjall::Snapshot, TapesReadTransaction), BlockchainError> {
464 loop {
465 let fjall = self.fjall.snapshot();
466 let tapes = self.linear_tapes.reader();
467
468 if self.tips_match(&fjall, &tapes)? {
469 return Ok((fjall, tapes));
470 }
471
472 }
474 }
475
476 pub fn make_consistent(&mut self) -> Result<(), BlockchainError> {
479 tracing::info!("Checking blockchain database consistency.");
480 let tips_match = {
481 let fjall = self.fjall.snapshot();
482 let tapes = self.linear_tapes.reader();
483 self.tips_match(&fjall, &tapes)?
484 };
485
486 if !tips_match {
487 tracing::warn!("fjall and tapes are out of sync");
488 self.rebuild_fjall_database()?;
489 }
490
491 if self.config.prune && matches!(self.prunable_tables, PrunableTables::Full(_)) {
493 self.enable_pruning()?;
494 }
495
496 Ok(())
497 }
498
499 pub fn rebuild_fjall_database(&mut self) -> Result<(), BlockchainError> {
503 self.block_heights = recreate_fjall_keyspace(&self.fjall, &self.block_heights)?;
504 self.chain_tip = recreate_fjall_keyspace(&self.fjall, &self.chain_tip)?;
505 self.key_images = recreate_fjall_keyspace(&self.fjall, &self.key_images)?;
506 self.pre_rct_outputs = recreate_fjall_keyspace(&self.fjall, &self.pre_rct_outputs)?;
507 self.tx_ids = recreate_fjall_keyspace(&self.fjall, &self.tx_ids)?;
508 self.v1_tx_outputs = recreate_fjall_keyspace(&self.fjall, &self.v1_tx_outputs)?;
509 reset_fjall_keyspace(&self.fjall, &self.alt_chain_infos)?;
510 reset_fjall_keyspace(&self.fjall, &self.alt_block_heights)?;
511 reset_fjall_keyspace(&self.fjall, &self.alt_block_infos)?;
512 reset_fjall_keyspace(&self.fjall, &self.alt_block_blobs)?;
513 reset_fjall_keyspace(&self.fjall, &self.alt_transaction_blobs)?;
514 reset_fjall_keyspace(&self.fjall, &self.alt_transaction_infos)?;
515
516 let rebuild_span = tracing::info_span!("rebuild_fjall_database");
517 let _guard = rebuild_span.enter();
518
519 tracing::info!("rebuilding fjall db");
520
521 let tapes_reader = self.linear_tapes.reader();
522
523 let tx_infos_iter = tapes_reader.iter_from(&self.tx_infos, 0)?;
524 let mut tx_iter = tx_infos_iter.map(|tx_info| {
525 let tx_info = tx_info.unwrap();
526
527 let mut tx_blob = vec![0; tx_info.pruned_size];
528 tapes_reader
529 .read_bytes(&self.pruned_blobs, tx_info.pruned_blob_idx, &mut tx_blob)
530 .unwrap();
531
532 let tx = Transaction::read(&mut tx_blob.as_slice()).unwrap();
533
534 Cow::Owned(tx)
535 });
536
537 let mut batch = self.fjall.batch().durability(Some(PersistMode::SyncAll));
538 let mut numb_txs = 0;
539 for height in 0..tapes_reader
540 .fixed_sized_tape_len(&self.block_infos)
541 .expect("block_infos tape exists")
542 {
543 let block =
544 crate::ops::block::get_block(&u64_to_usize(height), None, &tapes_reader, self)?;
545
546 let _miner_tx = tx_iter.next();
547
548 crate::ops::block::add_block_to_dynamic_tables(
549 self,
550 &block,
551 &block.hash(),
552 &mut tx_iter,
553 &mut numb_txs,
554 &mut batch,
555 &mut self.pre_rct_numb_outputs_cache.lock().unwrap(),
556 )?;
557
558 if height % 1000 == 0 {
559 tracing::info!("{} blocks processed", height);
560 let old_batch = mem::replace(
561 &mut batch,
562 self.fjall.batch().durability(Some(PersistMode::SyncAll)),
563 );
564
565 old_batch.commit()?;
566 }
567 }
568
569 batch.commit()?;
570
571 Ok(())
572 }
573
574 #[inline]
576 pub fn pruning_seed(&self) -> PruningSeed {
577 match &self.prunable_tables {
578 PrunableTables::Full(_) => PruningSeed::NotPruned,
579 PrunableTables::Pruned { stripe, .. } => {
580 PruningSeed::new_pruned(*stripe, CRYPTONOTE_PRUNING_LOG_STRIPES).unwrap()
581 }
582 }
583 }
584
585 fn enable_pruning(&mut self) -> Result<(), BlockchainError> {
589 let stripe = rand::thread_rng().gen_range(
591 1..=u32::try_from(PRUNABLE_BLOBS.len())
592 .expect("there shouldn't be that many prunable blobs"),
593 );
594
595 let PrunableTables::Full(prunable_blobs) =
597 mem::replace(&mut self.prunable_tables, PrunableTables::Full(vec![]))
598 else {
599 unreachable!("Database is already pruned");
600 };
601
602 tracing::info!("Pruning chain on stripe = {:?}.", stripe);
603
604 let mut tapes_append = self.linear_tapes.append();
607
608 let start_tip_height = tapes_append
609 .fixed_sized_tape_len(&self.block_infos)
610 .unwrap_or(0)
611 .saturating_sub(usize_to_u64(CRYPTONOTE_PRUNING_TIP_BLOCKS));
612 let start_tx_idx = tapes_append
613 .read_entry(&self.block_infos, start_tip_height)?
614 .map_or(0, |info| info.mining_tx_index);
615 let end_tx_idx = tapes_append
616 .fixed_sized_tape_len(&self.tx_infos)
617 .unwrap_or(0);
618
619 let prunable_tip_blobs = tapes_append.open_blob_tape(
620 "prunable_tip_blobs",
621 CachedTapeOpenOptions {
622 inner: RollingTapeOpenOptions {
623 file_size: PRUNABLE_BLOB_FILE_SIZE,
624 dir: self.config.blob_dir.clone(),
625 start_index: 0,
626 },
627 top_cache_size: self.config.cache_sizes.prunable_blobs,
628 },
629 )?;
630
631 let prunable_tip: FixedSizedTape<_, CachedBlobTape<RollingBlobTape>> = tapes_append
632 .open_fixed_sized_tape(
633 "prunable_tip",
634 CachedTapeOpenOptions {
635 inner: RollingTapeOpenOptions {
636 file_size: PRUNABLE_TIP_FILE_SIZE,
637 dir: self.config.index_dir.clone(),
638 start_index: start_tx_idx * usize_to_u64(size_of::<u64>()),
641 },
642 top_cache_size: self.config.cache_sizes.prunable_tip,
643 },
644 )?;
645
646 for tx_id in start_tx_idx..end_tx_idx {
648 let tx_info = tapes_append.read_entry(&self.tx_infos, tx_id)?.unwrap();
649
650 let block_stripe = cuprate_pruning::get_block_pruning_stripe(
651 tx_info.height,
652 usize::MAX,
653 CRYPTONOTE_PRUNING_LOG_STRIPES,
654 )
655 .unwrap();
656
657 let prunable_blob = &prunable_blobs
658 [usize::try_from(block_stripe).expect("stripe will not exceed usize::MAX") - 1];
659
660 let blob = if tx_info.is_v1_tx() {
663 vec![]
664 } else {
665 let mut b = vec![0; tx_info.prunable_size];
666 tapes_append.read_bytes(prunable_blob, tx_info.prunable_blob_idx, &mut b)?;
667 b
668 };
669
670 let idx = tapes_append.append_bytes(&prunable_tip_blobs, &blob)?;
671
672 let tx_id_2 = tapes_append.append_entries(&prunable_tip, &[idx])?;
673 assert_eq!(tx_id_2, tx_id);
675 }
676
677 let mut kept_stripe = None;
679 for (i, prunable_blob) in prunable_blobs.into_iter().enumerate() {
680 if u32_to_usize(stripe) - 1 == i {
681 kept_stripe = Some(prunable_blob);
682 } else {
683 tapes_append.delete_tape(prunable_blob);
684 }
685 }
686
687 tapes_append.commit(Persistence::SyncAll)?;
689
690 self.prunable_tables = PrunableTables::Pruned {
691 stripe,
692 prunable_tip_blobs,
693 prunable_tip,
694 kept_stripe: kept_stripe.unwrap(),
695 };
696
697 Ok(())
698 }
699}
700
701impl Drop for BlockchainDatabase {
702 fn drop(&mut self) {
703 tracing::info!(parent: &tracing::Span::none(), "Syncing blockchain database to storage.");
704
705 let _ = self.fjall.persist(PersistMode::SyncAll);
706
707 let _ = self.linear_tapes.append().commit(Persistence::SyncAll);
708 }
709}