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
20fn 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
31pub(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
42pub struct BlockchainDatabase {
44 pub(crate) config: Config,
46
47 pub(crate) linear_tapes: Tapes,
49 pub(crate) fjall: fjall::Database,
51
52 pub(crate) block_heights: fjall::Keyspace,
58 pub(crate) key_images: fjall::Keyspace,
64 pub(crate) pre_rct_outputs: fjall::Keyspace,
70 pub(crate) tx_ids: fjall::Keyspace,
76 pub(crate) v1_tx_outputs: fjall::Keyspace,
82 pub(crate) alt_chain_infos: ArcSwap<fjall::Keyspace>,
88 pub(crate) alt_block_heights: ArcSwap<fjall::Keyspace>,
94 pub(crate) alt_block_infos: ArcSwap<fjall::Keyspace>,
100 pub(crate) alt_block_blobs: ArcSwap<fjall::Keyspace>,
106 pub(crate) alt_transaction_blobs: ArcSwap<fjall::Keyspace>,
112 pub(crate) alt_transaction_infos: ArcSwap<fjall::Keyspace>,
118
119 pub(crate) rct_outputs: tapes::FixedSizedTape<RctOutput>,
125 pub(crate) tx_infos: tapes::FixedSizedTape<TxInfo>,
131 pub(crate) block_infos: tapes::FixedSizedTape<BlockInfo>,
137 pub(crate) pruned_blobs: tapes::BlobTape,
153 pub(crate) v1_prunable_blobs: tapes::BlobTape,
157 pub(crate) prunable_blobs: Vec<tapes::BlobTape>,
163
164 pub(crate) pre_rct_numb_outputs_cache: Mutex<HashMap<Amount, u64>>,
167}
168
169impl BlockchainDatabase {
170 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 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 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}