Skip to main content

cuprate_blockchain/
database.rs

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
27/// The key used to store the main-chain tip in [`BlockchainDatabase::chain_tip`].
28pub(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
44/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
45fn 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
55/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
56pub(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
66/// The blockchain database.
67pub struct BlockchainDatabase {
68    /// The database configuration.
69    pub(crate) config: Config,
70
71    /// The tapes database.
72    pub(crate) linear_tapes: Tapes,
73    /// The fjall database.
74    pub(crate) fjall: fjall::Database,
75
76    /// Block heights:
77    ///
78    /// | key                  | value                               |
79    /// |----------------------|-------------------------------------|
80    /// | block hash: [u8; 32] | block height: usize (little endian) |
81    pub(crate) block_heights: fjall::Keyspace,
82    /// Main-chain tip:
83    ///
84    /// | key               | value                |
85    /// |-------------------|----------------------|
86    /// | [`CHAIN_TIP_KEY`] | block hash: [u8; 32] |
87    pub(crate) chain_tip: fjall::Keyspace,
88    /// Key images:
89    ///
90    /// | key                 | value |
91    /// |---------------------|-------|
92    /// | key image: [u8; 32] | []    |
93    pub(crate) key_images: fjall::Keyspace,
94    /// Pre-RCT outputs:
95    ///
96    /// | key                                     | value                             |
97    /// |-----------------------------------------|-----------------------------------|
98    /// | The ID of the output [`PreRctOutputId`] | The output data: [`Output`] bytes |
99    pub(crate) pre_rct_outputs: fjall::Keyspace,
100    /// Transaction IDs:
101    ///
102    /// | key               | value                      |
103    /// |-------------------|----------------------------|
104    /// | Tx hash: [u8; 32] | Tx ID: u64 (little endian) |
105    pub(crate) tx_ids: fjall::Keyspace,
106    /// V1 transaction output amount indices:
107    ///
108    /// | key                        | value                                           |
109    /// |----------------------------|--------------------------------------------------|
110    /// | Tx ID: u64 (little endian) | amount indices as a [u64] (little endian) slice |
111    pub(crate) v1_tx_outputs: fjall::Keyspace,
112    /// Alt chain info:
113    ///
114    /// | key                           | value                  |
115    /// |-------------------------------|------------------------|
116    /// | Chain ID: u64 (little endian) | [`AltChainInfo`] bytes |
117    pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
118    /// Alt block heights:
119    ///
120    /// | key                  | value                    |
121    /// |----------------------|--------------------------|
122    /// | block hash: [u8; 32] | [`AltBlockHeight`] bytes |
123    pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
124    /// Alt block info:
125    ///
126    /// | key                        | value                          |
127    /// |----------------------------|--------------------------------|
128    /// | [`AltBlockHeight`] bytes   | [`CompactAltBlockInfo`] bytes  |
129    pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
130    /// Alt block blobs:
131    ///
132    /// | key                      | value            |
133    /// |--------------------------|------------------|
134    /// | [`AltBlockHeight`] bytes | block blob: [u8] |
135    pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
136    /// Alt transaction blobs:
137    ///
138    /// | key                        | value                       |
139    /// |----------------------------|-----------------------------|
140    /// | transaction hash: [u8; 32] | full transaction blob: [u8] |
141    pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
142    /// Alt transaction info:
143    ///
144    /// | key                        | value                        |
145    /// |----------------------------|------------------------------|
146    /// | transaction hash: [u8; 32] | [`AltTransactionInfo`] bytes |
147    pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
148
149    /// RCT (v2+) outputs, indexed sequentially.
150    ///
151    /// | index                 | value         |
152    /// |-----------------------|---------------|
153    /// | RCT output index: u64 | [`RctOutput`] |
154    pub(crate) rct_outputs: FixedSizedTape<RctOutput, CachedBlobTape<WholeBlobTape>>,
155    /// Transaction info, indexed by [`TxId`].
156    ///
157    /// | index      | value      |
158    /// |------------|------------|
159    /// | Tx ID: u64 | [`TxInfo`] |
160    pub(crate) tx_infos: FixedSizedTape<TxInfo, CachedBlobTape<WholeBlobTape>>,
161    /// Block info, indexed by block height.
162    ///
163    /// | index             | value         |
164    /// |-------------------|---------------|
165    /// | Block height: u64 | [`BlockInfo`] |
166    pub(crate) block_infos: FixedSizedTape<BlockInfo, CachedBlobTape<WholeBlobTape>>,
167    /// Pruned blobs.
168    ///
169    /// The format for this blob-tape per each block is:
170    ///
171    /// | data                                       |
172    /// |--------------------------------------------|
173    /// | block blob (header, miner tx, tx hashes)   |
174    /// | tx 0 pruned blob                           |
175    /// | tx 0 prunable hash (32 bytes)              |
176    /// | tx 1 pruned blob                           |
177    /// | tx 1 prunable hash (32 bytes)              |
178    /// | ...                                        |
179    ///
180    /// The prunable hash is `[0; 32]` for v1 txs.
181    /// Each block is appended directly after the one before it.
182    pub(crate) pruned_blobs: CachedBlobTape<WholeBlobTape>,
183    /// V1 prunable transaction blobs, indexed by [`TxInfo::prunable_blob_idx`].
184    ///
185    /// This tape stores the prunable blob for all V1 txs, these can't be pruned.
186    pub(crate) v1_prunable_blobs: CachedBlobTape<WholeBlobTape>,
187    /// The pruning tables of this database.
188    pub(crate) prunable_tables: PrunableTables,
189
190    /// A runtime cache of the number of outputs for each pre-rct output amount.
191    /// This is filled in lazily.
192    pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
193}
194
195/// The pruning state of the database.
196pub(crate) enum PrunableTables {
197    /// An unpruned database.
198    ///
199    /// These tapes store the prunable part of each tx, the stripe a tx is stored in depends on the
200    /// height of the block.
201    Full(Vec<CachedBlobTape<WholeBlobTape>>),
202    /// A pruned database.
203    Pruned {
204        /// The stripe we keep.
205        stripe: u32,
206        /// The kept stripe tape.
207        kept_stripe: CachedBlobTape<WholeBlobTape>,
208        /// Prunable tip index, indexed by [`TxId`]
209        ///
210        /// # Warning
211        ///
212        /// Only transactions in pruned blocks (`block_stripe` != `stripe`) will have a valid value
213        /// in this table. Other txs will be here but their value is unspecified.
214        ///
215        /// | index | value                                   |
216        /// |-------|------------------------------------------|
217        /// | Tx ID | An index (u64) into `prunable_tip_blobs` |
218        prunable_tip: FixedSizedTape<u64, CachedBlobTape<RollingBlobTape>>,
219        /// The prunable blobs of txs in blocks that will be pruned.
220        prunable_tip_blobs: CachedBlobTape<RollingBlobTape>,
221    },
222}
223
224impl PrunableTables {
225    /// Attempt to get the full blob tape for the given stripe, returning [`None`] if
226    /// we pruned it.
227    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    /// Opens a [`PrunableTables`] using the given tapes transaction.
243    fn open(
244        tape_append_tx: &mut TapesAppendTransaction,
245        config: &Config,
246    ) -> Result<Self, BlockchainError> {
247        // `prunable_tip` is a tape that is only created when pruned.
248        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            // Only the stripe we keep will be in the tapes' database.
259            // TODO: the tape file of a deleted tape could be left over eating up space in a crash though.
260            // Add a way to check and delete tape files so we can make sure they are removed here.
261            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            // Open the tip tapes. We can use `start_index: 0` here as the tapes database will
279            // only take that value into account when creating a new tape. Here we are always opening
280            // an already existing tape.
281            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    /// Open a [`BlockchainDatabase`] with an [`fjall::Database`] for storing data that can't be stored in tapes.
328    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    /// Returns whether Fjall and Tapes are at the same main-chain tip.
431    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            // Old fjall DBs do not have the `chain_tip` key space, with the pruning upgrade this would
452            // make cuprate think an empty tapes and full fjall DB is in sync. So for now check that
453            // fjall is really empty.
454            (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    /// Returns Fjall and Tapes read transactions at the same main-chain tip.
461    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            // TODO: bound this and panic if we can't get the txs to agree.
473        }
474    }
475
476    /// Checks if the fjall and tapes database are in sync and rebuilds the fjall database if it
477    /// is not.
478    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 we are pruning and have not yet pruned then prune.
492        if self.config.prune && matches!(self.prunable_tables, PrunableTables::Full(_)) {
493            self.enable_pruning()?;
494        }
495
496        Ok(())
497    }
498
499    /// Rebuilds the fjall database.
500    ///
501    /// This will not fill in the prunable tip blocks.
502    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    /// Returns the [`PruningSeed`] for this database.
575    #[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    /// - generate new [`PruningSeed`] (if one doesn't exist)
586    /// - populate [`BlockchainDatabase::prunable_tip`] with latest blocks
587    /// - delete unnecessary [`BlockchainDatabase::prunable_blobs`]
588    fn enable_pruning(&mut self) -> Result<(), BlockchainError> {
589        // generate a random stripe index to prune
590        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        // Take the prunable tables, we will set it again before returning Ok.
596        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        // This transaction is the only transaction for the whole pruning process, it will atomically
605        // prune the DB.
606        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                        // We want to start the indexing at the tx index of the first tip tx.
639                        // start_index is measured in raw bytes.
640                        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        // fill in the tip tapes.
647        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            // V1 txs are always unpruned, but we still need to store them in `prunable_tip` to keep the
661            // index tracking of that tape correct.
662            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            // Make sure the index is what we expect.
674            assert_eq!(tx_id_2, tx_id);
675        }
676
677        // Delete the tapes we no longer need.
678        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        // Prune the DB!
688        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}