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};
9use monero_oxide::transaction::Transaction;
10use tapes::{Persistence, TapeOpenOptions, Tapes, TapesRead};
11
12use cuprate_helper::cast::{u64_to_usize, usize_to_u64};
13
14use crate::{
15    config::Config,
16    types::{Amount, BlockInfo, RctOutput, TxInfo},
17    BlockchainError,
18};
19
20/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
21fn recreate_fjall_keyspace(
22    database: &fjall::Database,
23    keyspace: &fjall::Keyspace,
24) -> Result<fjall::Keyspace, BlockchainError> {
25    let name = keyspace.name().to_string();
26
27    database.delete_keyspace(keyspace.clone())?;
28    Ok(database.keyspace(&name, KeyspaceCreateOptions::default)?)
29}
30
31/// Deletes a [`fjall::Keyspace`] and recreates it with the same name.
32pub(crate) fn reset_fjall_keyspace(
33    database: &fjall::Database,
34    keyspace: &ArcSwap<fjall::Keyspace>,
35) -> Result<(), BlockchainError> {
36    let new_keyspace = recreate_fjall_keyspace(database, &keyspace.load())?;
37    keyspace.store(Arc::new(new_keyspace));
38
39    Ok(())
40}
41
42/// The blockchain database.
43pub struct BlockchainDatabase {
44    /// The database configuration.
45    pub(crate) config: Config,
46
47    /// The tapes database.
48    pub(crate) linear_tapes: Tapes,
49    /// The fjall database.
50    pub(crate) fjall: fjall::Database,
51
52    /// Block heights:
53    ///
54    /// | key                  | value                               |
55    /// |----------------------|-------------------------------------|
56    /// | block hash: [u8; 32] | block height: usize (little endian) |
57    pub(crate) block_heights: fjall::Keyspace,
58    /// Key images:
59    ///
60    /// | key                 | value |
61    /// |---------------------|-------|
62    /// | key image: [u8; 32] | []    |
63    pub(crate) key_images: fjall::Keyspace,
64    /// Pre-RCT outputs:
65    ///
66    /// | key                                     | value                             |
67    /// |-----------------------------------------|-----------------------------------|
68    /// | The ID of the output [`PreRctOutputId`] | The output data: [`Output`] bytes |
69    pub(crate) pre_rct_outputs: fjall::Keyspace,
70    /// Transaction IDs:
71    ///
72    /// | key               | value                      |
73    /// |-------------------|----------------------------|
74    /// | Tx hash: [u8; 32] | Tx ID: u64 (little endian) |
75    pub(crate) tx_ids: fjall::Keyspace,
76    /// V1 transaction output amount indices:
77    ///
78    /// | key                        | value                                           |
79    /// |----------------------------|--------------------------------------------------|
80    /// | Tx ID: u64 (little endian) | amount indices as a [u64] (little endian) slice |
81    pub(crate) v1_tx_outputs: fjall::Keyspace,
82    /// Alt chain info:
83    ///
84    /// | key                           | value                  |
85    /// |-------------------------------|------------------------|
86    /// | Chain ID: u64 (little endian) | [`AltChainInfo`] bytes |
87    pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
88    /// Alt block heights:
89    ///
90    /// | key                  | value                    |
91    /// |----------------------|--------------------------|
92    /// | block hash: [u8; 32] | [`AltBlockHeight`] bytes |
93    pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
94    /// Alt block info:
95    ///
96    /// | key                        | value                          |
97    /// |----------------------------|--------------------------------|
98    /// | [`AltBlockHeight`] bytes   | [`CompactAltBlockInfo`] bytes  |
99    pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
100    /// Alt block blobs:
101    ///
102    /// | key                      | value            |
103    /// |--------------------------|------------------|
104    /// | [`AltBlockHeight`] bytes | block blob: [u8] |
105    pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
106    /// Alt transaction blobs:
107    ///
108    /// | key                        | value                       |
109    /// |----------------------------|-----------------------------|
110    /// | transaction hash: [u8; 32] | full transaction blob: [u8] |
111    pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
112    /// Alt transaction info:
113    ///
114    /// | key                        | value                        |
115    /// |----------------------------|------------------------------|
116    /// | transaction hash: [u8; 32] | [`AltTransactionInfo`] bytes |
117    pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
118
119    /// RCT (v2+) outputs, indexed sequentially.
120    ///
121    /// | index                 | value         |
122    /// |-----------------------|---------------|
123    /// | RCT output index: u64 | [`RctOutput`] |
124    pub(crate) rct_outputs: tapes::FixedSizedTape<RctOutput>,
125    /// Transaction info, indexed by [`TxId`].
126    ///
127    /// | index      | value      |
128    /// |------------|------------|
129    /// | Tx ID: u64 | [`TxInfo`] |
130    pub(crate) tx_infos: tapes::FixedSizedTape<TxInfo>,
131    /// Block info, indexed by block height.
132    ///
133    /// | index             | value         |
134    /// |-------------------|---------------|
135    /// | Block height: u64 | [`BlockInfo`] |
136    pub(crate) block_infos: tapes::FixedSizedTape<BlockInfo>,
137    /// Pruned blobs.
138    ///
139    /// The format for this blob-tape per each block is:
140    ///
141    /// | data                                       |
142    /// |--------------------------------------------|
143    /// | block blob (header, miner tx, tx hashes)   |
144    /// | tx 0 pruned blob                           |
145    /// | tx 0 prunable hash (32 bytes)              |
146    /// | tx 1 pruned blob                           |
147    /// | tx 1 prunable hash (32 bytes)              |
148    /// | ...                                        |
149    ///
150    /// The prunable hash is `[0; 32]` for v1 txs.
151    /// Each block is appended directly after the one before it.
152    pub(crate) pruned_blobs: tapes::BlobTape,
153    /// V1 prunable transaction blobs, indexed by [`TxInfo::prunable_blob_idx`].
154    ///
155    /// This tape stores the prunable blob for all V1 txs, these can't be pruned.
156    pub(crate) v1_prunable_blobs: tapes::BlobTape,
157    /// V2+ prunable transaction blobs, split across 8 stripes.
158    /// Indexed by [`TxInfo::prunable_blob_idx`].
159    ///
160    /// These tapes store the prunable part of each tx, the stripe a tx is stored in depends on the
161    /// height of the block.
162    pub(crate) prunable_blobs: Vec<tapes::BlobTape>,
163
164    /// A runtime cache of the number of outputs for each pre-rct output amount.
165    /// This is filled in lazily.
166    pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
167}
168
169impl BlockchainDatabase {
170    /// Open a [`BlockchainDatabase`] with an [`fjall::Database`] for storing data that can't be stored in tapes.
171    pub fn open_with_fjall_database(
172        config: &Config,
173        fjall: fjall::Database,
174    ) -> Result<Self, BlockchainError> {
175        let block_heights = fjall.keyspace("block_heights", KeyspaceCreateOptions::default)?;
176        let key_images = fjall.keyspace("key_images", KeyspaceCreateOptions::default)?;
177        let pre_rct_outputs = fjall.keyspace("pre_rct_outputs", KeyspaceCreateOptions::default)?;
178        let tx_ids = fjall.keyspace("tx_ids", KeyspaceCreateOptions::default)?;
179        let v1_tx_outputs = fjall.keyspace("tx_outputs", KeyspaceCreateOptions::default)?;
180
181        let alt_chain_infos = fjall.keyspace("alt_chain_infos", KeyspaceCreateOptions::default)?;
182        let alt_block_heights =
183            fjall.keyspace("alt_block_heights", KeyspaceCreateOptions::default)?;
184        let alt_block_infos = fjall.keyspace("alt_block_infos", KeyspaceCreateOptions::default)?;
185        let alt_block_blobs = fjall.keyspace("alt_block_blobs", KeyspaceCreateOptions::default)?;
186        let alt_transaction_blobs =
187            fjall.keyspace("alt_transaction_blobs", KeyspaceCreateOptions::default)?;
188        let alt_transaction_infos =
189            fjall.keyspace("alt_transaction_infos", KeyspaceCreateOptions::default)?;
190
191        let tapes_index_dir = config.index_dir.join("tapes");
192        let tapes_blob_dir = config.blob_dir.join("tapes");
193
194        let linear_tapes = Tapes::open(&tapes_index_dir)?;
195        let mut tape_append_tx = linear_tapes.append();
196
197        let rct_outputs = tape_append_tx.open_fixed_sized_tape(
198            "rct_outputs",
199            &TapeOpenOptions {
200                top_cache_size: config.cache_sizes.rct_outputs,
201                dir: tapes_index_dir.clone(),
202            },
203        )?;
204        let tx_infos = tape_append_tx.open_fixed_sized_tape(
205            "tx_infos",
206            &TapeOpenOptions {
207                top_cache_size: config.cache_sizes.tx_infos,
208                dir: tapes_index_dir.clone(),
209            },
210        )?;
211        let block_infos = tape_append_tx.open_fixed_sized_tape(
212            "block_infos",
213            &TapeOpenOptions {
214                top_cache_size: config.cache_sizes.block_infos,
215                dir: tapes_index_dir,
216            },
217        )?;
218        let pruned_blobs = tape_append_tx.open_blob_tape(
219            "pruned_blobs",
220            &TapeOpenOptions {
221                top_cache_size: config.cache_sizes.pruned_blobs,
222                dir: tapes_blob_dir.clone(),
223            },
224        )?;
225        let v1_prunable_blobs = tape_append_tx.open_blob_tape(
226            "v1_prunable_blobs",
227            &TapeOpenOptions {
228                top_cache_size: config.cache_sizes.v1_prunable_blobs,
229                dir: tapes_blob_dir.clone(),
230            },
231        )?;
232
233        const PRUNABLE_BLOBS: [&str; 8] = [
234            "prunable1",
235            "prunable2",
236            "prunable3",
237            "prunable4",
238            "prunable5",
239            "prunable6",
240            "prunable7",
241            "prunable8",
242        ];
243
244        let prunable_blobs = (0..8)
245            .map(|i| {
246                tape_append_tx.open_blob_tape(
247                    PRUNABLE_BLOBS[i],
248                    &TapeOpenOptions {
249                        top_cache_size: config.cache_sizes.prunable_blobs,
250                        dir: tapes_blob_dir.clone(),
251                    },
252                )
253            })
254            .collect::<Result<_, _>>()?;
255
256        tape_append_tx.commit(Persistence::SyncAll)?;
257
258        tracing::debug!("opened db");
259        Ok(Self {
260            fjall,
261            linear_tapes,
262            config: config.clone(),
263            block_heights,
264            key_images,
265            pre_rct_outputs,
266            tx_ids,
267            v1_tx_outputs,
268            alt_chain_infos: ArcSwap::from_pointee(alt_chain_infos),
269            alt_block_heights: ArcSwap::from_pointee(alt_block_heights),
270            alt_block_infos: ArcSwap::from_pointee(alt_block_infos),
271            alt_block_blobs: ArcSwap::from_pointee(alt_block_blobs),
272            alt_transaction_blobs: ArcSwap::from_pointee(alt_transaction_blobs),
273            alt_transaction_infos: ArcSwap::from_pointee(alt_transaction_infos),
274            rct_outputs,
275            tx_infos,
276            block_infos,
277            pruned_blobs,
278            v1_prunable_blobs,
279            prunable_blobs,
280            pre_rct_numb_outputs_cache: Mutex::new(HashMap::new()),
281        })
282    }
283
284    /// Checks if the fjall and tapes database are in sync and rebuilds the fjall database if it
285    /// is not.
286    pub fn make_consistent(&mut self) -> Result<(), BlockchainError> {
287        tracing::info!("Checking blockchain database consistency.");
288
289        let tapes_height = self
290            .linear_tapes
291            .reader()
292            .fixed_sized_tape_len(&self.block_infos)
293            .expect("block_infos tape exists");
294
295        if tapes_height != usize_to_u64(self.block_heights.len()?) {
296            tracing::warn!("fjall and tapes are out of sync");
297            self.rebuild_fjall_database()?;
298        }
299
300        Ok(())
301    }
302
303    /// Rebuilds the fjall database.
304    pub fn rebuild_fjall_database(&mut self) -> Result<(), BlockchainError> {
305        self.block_heights = recreate_fjall_keyspace(&self.fjall, &self.block_heights)?;
306        self.key_images = recreate_fjall_keyspace(&self.fjall, &self.key_images)?;
307        self.pre_rct_outputs = recreate_fjall_keyspace(&self.fjall, &self.pre_rct_outputs)?;
308        self.tx_ids = recreate_fjall_keyspace(&self.fjall, &self.tx_ids)?;
309        self.v1_tx_outputs = recreate_fjall_keyspace(&self.fjall, &self.v1_tx_outputs)?;
310        reset_fjall_keyspace(&self.fjall, &self.alt_chain_infos)?;
311        reset_fjall_keyspace(&self.fjall, &self.alt_block_heights)?;
312        reset_fjall_keyspace(&self.fjall, &self.alt_block_infos)?;
313        reset_fjall_keyspace(&self.fjall, &self.alt_block_blobs)?;
314        reset_fjall_keyspace(&self.fjall, &self.alt_transaction_blobs)?;
315        reset_fjall_keyspace(&self.fjall, &self.alt_transaction_infos)?;
316
317        let rebuild_span = tracing::info_span!("rebuild_fjall_database");
318        let _guard = rebuild_span.enter();
319
320        tracing::info!("rebuilding fjall db");
321
322        let tapes_reader = self.linear_tapes.reader();
323
324        let tx_infos_iter = tapes_reader.iter_from(&self.tx_infos, 0)?;
325        let mut tx_iter = tx_infos_iter.map(|tx_info| {
326            let tx_info = tx_info.unwrap();
327
328            let mut tx_blob = vec![0; tx_info.pruned_size];
329            tapes_reader
330                .read_bytes(&self.pruned_blobs, tx_info.pruned_blob_idx, &mut tx_blob)
331                .unwrap();
332
333            let tx = Transaction::read(&mut tx_blob.as_slice()).unwrap();
334
335            Cow::Owned(tx)
336        });
337
338        let mut batch = self.fjall.batch().durability(Some(PersistMode::Buffer));
339        let mut numb_txs = 0;
340        for height in 0..tapes_reader
341            .fixed_sized_tape_len(&self.block_infos)
342            .expect("block_infos tape exists")
343        {
344            let block =
345                crate::ops::block::get_block(&u64_to_usize(height), None, &tapes_reader, self)?;
346
347            let _miner_tx = tx_iter.next();
348
349            crate::ops::block::add_block_to_dynamic_tables(
350                self,
351                &block,
352                &block.hash(),
353                &mut tx_iter,
354                &mut numb_txs,
355                &mut batch,
356                &mut self.pre_rct_numb_outputs_cache.lock().unwrap(),
357            )?;
358
359            if height % 1000 == 0 {
360                tracing::info!("{} blocks processed", height);
361                let old_batch = std::mem::replace(
362                    &mut batch,
363                    self.fjall.batch().durability(Some(PersistMode::Buffer)),
364                );
365
366                old_batch.commit()?;
367            }
368        }
369
370        batch.commit()?;
371
372        Ok(())
373    }
374}
375
376impl Drop for BlockchainDatabase {
377    fn drop(&mut self) {
378        tracing::info!(parent: &tracing::Span::none(), "Syncing blockchain database to storage.");
379
380        let _ = self.fjall.persist(PersistMode::SyncAll);
381
382        let _ = self.linear_tapes.append().commit(Persistence::SyncAll);
383    }
384}