Skip to main content

cuprate_blockchain/
database.rs

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
20/// The key used to store the main-chain tip in [`BlockchainDatabase::chain_tip`].
21pub(crate) const CHAIN_TIP_KEY: &[u8] = b"tip";
22
23/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
24fn 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
34/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
35pub(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
45/// The blockchain database.
46pub struct BlockchainDatabase {
47    /// The database configuration.
48    pub(crate) config: Config,
49
50    /// The tapes database.
51    pub(crate) linear_tapes: Tapes,
52    /// The fjall database.
53    pub(crate) fjall: fjall::Database,
54
55    /// Block heights:
56    ///
57    /// | key                  | value                               |
58    /// |----------------------|-------------------------------------|
59    /// | block hash: [u8; 32] | block height: usize (little endian) |
60    pub(crate) block_heights: fjall::Keyspace,
61    /// Main-chain tip:
62    ///
63    /// | key               | value                |
64    /// |-------------------|----------------------|
65    /// | [`CHAIN_TIP_KEY`] | block hash: [u8; 32] |
66    pub(crate) chain_tip: fjall::Keyspace,
67    /// Key images:
68    ///
69    /// | key                 | value |
70    /// |---------------------|-------|
71    /// | key image: [u8; 32] | []    |
72    pub(crate) key_images: fjall::Keyspace,
73    /// Pre-RCT outputs:
74    ///
75    /// | key                                     | value                             |
76    /// |-----------------------------------------|-----------------------------------|
77    /// | The ID of the output [`PreRctOutputId`] | The output data: [`Output`] bytes |
78    pub(crate) pre_rct_outputs: fjall::Keyspace,
79    /// Transaction IDs:
80    ///
81    /// | key               | value                      |
82    /// |-------------------|----------------------------|
83    /// | Tx hash: [u8; 32] | Tx ID: u64 (little endian) |
84    pub(crate) tx_ids: fjall::Keyspace,
85    /// V1 transaction output amount indices:
86    ///
87    /// | key                        | value                                           |
88    /// |----------------------------|--------------------------------------------------|
89    /// | Tx ID: u64 (little endian) | amount indices as a [u64] (little endian) slice |
90    pub(crate) v1_tx_outputs: fjall::Keyspace,
91    /// Alt chain info:
92    ///
93    /// | key                           | value                  |
94    /// |-------------------------------|------------------------|
95    /// | Chain ID: u64 (little endian) | [`AltChainInfo`] bytes |
96    pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
97    /// Alt block heights:
98    ///
99    /// | key                  | value                    |
100    /// |----------------------|--------------------------|
101    /// | block hash: [u8; 32] | [`AltBlockHeight`] bytes |
102    pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
103    /// Alt block info:
104    ///
105    /// | key                        | value                          |
106    /// |----------------------------|--------------------------------|
107    /// | [`AltBlockHeight`] bytes   | [`CompactAltBlockInfo`] bytes  |
108    pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
109    /// Alt block blobs:
110    ///
111    /// | key                      | value            |
112    /// |--------------------------|------------------|
113    /// | [`AltBlockHeight`] bytes | block blob: [u8] |
114    pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
115    /// Alt transaction blobs:
116    ///
117    /// | key                        | value                       |
118    /// |----------------------------|-----------------------------|
119    /// | transaction hash: [u8; 32] | full transaction blob: [u8] |
120    pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
121    /// Alt transaction info:
122    ///
123    /// | key                        | value                        |
124    /// |----------------------------|------------------------------|
125    /// | transaction hash: [u8; 32] | [`AltTransactionInfo`] bytes |
126    pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
127
128    /// RCT (v2+) outputs, indexed sequentially.
129    ///
130    /// | index                 | value         |
131    /// |-----------------------|---------------|
132    /// | RCT output index: u64 | [`RctOutput`] |
133    pub(crate) rct_outputs: tapes::FixedSizedTape<RctOutput>,
134    /// Transaction info, indexed by [`TxId`].
135    ///
136    /// | index      | value      |
137    /// |------------|------------|
138    /// | Tx ID: u64 | [`TxInfo`] |
139    pub(crate) tx_infos: tapes::FixedSizedTape<TxInfo>,
140    /// Block info, indexed by block height.
141    ///
142    /// | index             | value         |
143    /// |-------------------|---------------|
144    /// | Block height: u64 | [`BlockInfo`] |
145    pub(crate) block_infos: tapes::FixedSizedTape<BlockInfo>,
146    /// Pruned blobs.
147    ///
148    /// The format for this blob-tape per each block is:
149    ///
150    /// | data                                       |
151    /// |--------------------------------------------|
152    /// | block blob (header, miner tx, tx hashes)   |
153    /// | tx 0 pruned blob                           |
154    /// | tx 0 prunable hash (32 bytes)              |
155    /// | tx 1 pruned blob                           |
156    /// | tx 1 prunable hash (32 bytes)              |
157    /// | ...                                        |
158    ///
159    /// The prunable hash is `[0; 32]` for v1 txs.
160    /// Each block is appended directly after the one before it.
161    pub(crate) pruned_blobs: tapes::BlobTape,
162    /// V1 prunable transaction blobs, indexed by [`TxInfo::prunable_blob_idx`].
163    ///
164    /// This tape stores the prunable blob for all V1 txs, these can't be pruned.
165    pub(crate) v1_prunable_blobs: tapes::BlobTape,
166    /// V2+ prunable transaction blobs, split across 8 stripes.
167    /// Indexed by [`TxInfo::prunable_blob_idx`].
168    ///
169    /// These tapes store the prunable part of each tx, the stripe a tx is stored in depends on the
170    /// height of the block.
171    pub(crate) prunable_blobs: Vec<tapes::BlobTape>,
172
173    /// A runtime cache of the number of outputs for each pre-rct output amount.
174    /// This is filled in lazily.
175    pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
176}
177
178impl BlockchainDatabase {
179    /// Open a [`BlockchainDatabase`] with an [`fjall::Database`] for storing data that can't be stored in tapes.
180    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    /// Returns whether Fjall and Tapes are at the same main-chain tip.
296    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    /// Returns Fjall and Tapes read transactions at the same main-chain tip.
323    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            // TODO: bound this and panic if we can't get the txs to agree.
335        }
336    }
337
338    /// Checks if the fjall and tapes database are in sync and rebuilds the fjall database if it
339    /// is not.
340    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    /// Rebuilds the fjall database.
358    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}