1#[cfg(feature = "rocksdb")]
2use crate::state::{
3 historical_rocksdb::StateRewindPolicy,
4 rocks_db::DatabaseConfig,
5};
6
7use crate::{
8 database::{
9 Database,
10 GenesisDatabase,
11 Result as DatabaseResult,
12 database_description::{
13 compression::CompressionDatabase,
14 gas_price::GasPriceDatabase,
15 off_chain::OffChain,
16 on_chain::OnChain,
17 relayer::Relayer,
18 },
19 },
20 service::DbType,
21};
22
23#[cfg(feature = "rpc")]
24use crate::database::database_description::block_aggregator::BlockAggregatorDatabase;
25#[cfg(feature = "test-helpers")]
26use fuel_core_chain_config::{
27 StateConfig,
28 StateConfigBuilder,
29};
30#[cfg(feature = "backup")]
31use fuel_core_services::TraceErr;
32use fuel_core_storage::Result as StorageResult;
33#[cfg(feature = "test-helpers")]
34use fuel_core_storage::tables::{
35 Coins,
36 ContractsAssets,
37 ContractsLatestUtxo,
38 ContractsRawCode,
39 ContractsState,
40 Messages,
41};
42use fuel_core_types::{
43 blockchain::primitives::DaBlockHeight,
44 fuel_types::BlockHeight,
45};
46use std::path::PathBuf;
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct CombinedDatabaseConfig {
50 pub database_path: PathBuf,
51 pub database_type: DbType,
52 #[cfg(feature = "rocksdb")]
53 pub database_config: DatabaseConfig,
54 #[cfg(feature = "rocksdb")]
55 pub state_rewind_policy: StateRewindPolicy,
56}
57
58#[derive(Default, Clone)]
60pub struct CombinedDatabase {
61 on_chain: Database<OnChain>,
62 off_chain: Database<OffChain>,
63 relayer: Database<Relayer>,
64 gas_price: Database<GasPriceDatabase>,
65 compression: Database<CompressionDatabase>,
66 #[cfg(feature = "rpc")]
67 block_aggregation_storage: Database<BlockAggregatorDatabase>,
68}
69
70impl CombinedDatabase {
71 pub fn new(
72 on_chain: Database<OnChain>,
73 off_chain: Database<OffChain>,
74 relayer: Database<Relayer>,
75 gas_price: Database<GasPriceDatabase>,
76 compression: Database<CompressionDatabase>,
77 #[cfg(feature = "rpc")] block_aggregation_storage: Database<
78 BlockAggregatorDatabase,
79 >,
80 ) -> Self {
81 Self {
82 on_chain,
83 off_chain,
84 relayer,
85 gas_price,
86 compression,
87 #[cfg(feature = "rpc")]
88 block_aggregation_storage,
89 }
90 }
91
92 #[cfg(feature = "rocksdb")]
93 pub fn prune(path: &std::path::Path) -> crate::database::Result<()> {
94 crate::state::rocks_db::RocksDb::<OnChain>::prune(path)?;
95 crate::state::rocks_db::RocksDb::<OffChain>::prune(path)?;
96 crate::state::rocks_db::RocksDb::<Relayer>::prune(path)?;
97 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::prune(path)?;
98 crate::state::rocks_db::RocksDb::<CompressionDatabase>::prune(path)?;
99 #[cfg(feature = "rpc")]
100 crate::state::rocks_db::RocksDb::<BlockAggregatorDatabase>::prune(path)?;
101 Ok(())
102 }
103
104 #[cfg(feature = "backup")]
105 pub fn backup(
106 db_dir: &std::path::Path,
107 backup_dir: &std::path::Path,
108 ) -> crate::database::Result<()> {
109 use tempfile::TempDir;
110
111 let temp_backup_dir = TempDir::new()
112 .trace_err("Failed to create temporary backup directory")
113 .map_err(|e| anyhow::anyhow!(e))?;
114
115 Self::backup_databases(db_dir, temp_backup_dir.path())?;
116
117 std::fs::rename(temp_backup_dir.path(), backup_dir)
118 .trace_err("Failed to move temporary backup directory")
119 .map_err(|e| anyhow::anyhow!(e))?;
120
121 Ok(())
122 }
123
124 #[cfg(feature = "backup")]
125 fn backup_databases(
126 db_dir: &std::path::Path,
127 temp_dir: &std::path::Path,
128 ) -> crate::database::Result<()> {
129 crate::state::rocks_db::RocksDb::<OnChain>::backup(db_dir, temp_dir)
130 .trace_err("Failed to backup on-chain database")?;
131
132 crate::state::rocks_db::RocksDb::<OffChain>::backup(db_dir, temp_dir)
133 .trace_err("Failed to backup off-chain database")?;
134
135 crate::state::rocks_db::RocksDb::<Relayer>::backup(db_dir, temp_dir)
136 .trace_err("Failed to backup relayer database")?;
137
138 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::backup(db_dir, temp_dir)
139 .trace_err("Failed to backup gas-price database")?;
140
141 crate::state::rocks_db::RocksDb::<CompressionDatabase>::backup(db_dir, temp_dir)
142 .trace_err("Failed to backup compression database")?;
143
144 #[cfg(feature = "rpc")]
145 crate::state::rocks_db::RocksDb::<BlockAggregatorDatabase>::backup(
146 db_dir, temp_dir,
147 )
148 .trace_err("Failed to backup block aggregation storage database")?;
149
150 Ok(())
151 }
152
153 #[cfg(feature = "backup")]
154 pub fn restore(
155 restore_to: &std::path::Path,
156 backup_dir: &std::path::Path,
157 ) -> crate::database::Result<()> {
158 use tempfile::TempDir;
159
160 let temp_restore_dir = TempDir::new()
161 .trace_err("Failed to create temporary restore directory")
162 .map_err(|e| anyhow::anyhow!(e))?;
163
164 Self::restore_database(backup_dir, temp_restore_dir.path())?;
165
166 std::fs::rename(temp_restore_dir.path(), restore_to)
167 .trace_err("Failed to move temporary restore directory")
168 .map_err(|e| anyhow::anyhow!(e))?;
169
170 Ok(())
173 }
174
175 #[cfg(feature = "backup")]
176 fn restore_database(
177 backup_dir: &std::path::Path,
178 temp_restore_dir: &std::path::Path,
179 ) -> crate::database::Result<()> {
180 crate::state::rocks_db::RocksDb::<OnChain>::restore(temp_restore_dir, backup_dir)
181 .trace_err("Failed to restore on-chain database")?;
182
183 crate::state::rocks_db::RocksDb::<OffChain>::restore(
184 temp_restore_dir,
185 backup_dir,
186 )
187 .trace_err("Failed to restore off-chain database")?;
188
189 crate::state::rocks_db::RocksDb::<Relayer>::restore(temp_restore_dir, backup_dir)
190 .trace_err("Failed to restore relayer database")?;
191
192 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::restore(
193 temp_restore_dir,
194 backup_dir,
195 )
196 .trace_err("Failed to restore gas-price database")?;
197
198 crate::state::rocks_db::RocksDb::<CompressionDatabase>::restore(
199 temp_restore_dir,
200 backup_dir,
201 )
202 .trace_err("Failed to restore compression database")?;
203
204 #[cfg(feature = "rpc")]
205 crate::state::rocks_db::RocksDb::<BlockAggregatorDatabase>::restore(
206 temp_restore_dir,
207 backup_dir,
208 )
209 .trace_err("Failed to restore block aggregation storage database")?;
210
211 Ok(())
212 }
213
214 #[cfg(feature = "rocksdb")]
215 pub fn open(
216 path: &std::path::Path,
217 state_rewind_policy: StateRewindPolicy,
218 database_config: DatabaseConfig,
219 ) -> crate::database::Result<Self> {
220 let max_fds = match database_config.max_fds {
223 -1 => -1,
224 _ => database_config.max_fds.saturating_div(4),
225 };
226
227 let on_chain = Database::open_rocksdb(
229 path,
230 state_rewind_policy,
231 DatabaseConfig {
232 max_fds,
233 ..database_config
234 },
235 )?;
236 let off_chain = Database::open_rocksdb(
237 path,
238 state_rewind_policy,
239 DatabaseConfig {
240 max_fds,
241 ..database_config
242 },
243 )?;
244 let relayer = Database::open_rocksdb(
245 path,
246 state_rewind_policy,
247 DatabaseConfig {
248 max_fds,
249 ..database_config
250 },
251 )?;
252 let gas_price = Database::open_rocksdb(
253 path,
254 state_rewind_policy,
255 DatabaseConfig {
256 max_fds,
257 ..database_config
258 },
259 )?;
260 let compression = Database::open_rocksdb(
261 path,
262 state_rewind_policy,
263 DatabaseConfig {
264 max_fds,
265 ..database_config
266 },
267 )?;
268 #[cfg(feature = "rpc")]
269 let block_aggregation_storage = Database::open_rocksdb(
270 path,
271 state_rewind_policy,
272 DatabaseConfig {
273 max_fds,
274 ..database_config
275 },
276 )?;
277
278 Ok(Self {
279 on_chain,
280 off_chain,
281 relayer,
282 gas_price,
283 compression,
284 #[cfg(feature = "rpc")]
285 block_aggregation_storage,
286 })
287 }
288
289 #[cfg(feature = "rocksdb")]
291 pub fn temp_database_with_state_rewind_policy(
292 state_rewind_policy: StateRewindPolicy,
293 database_config: DatabaseConfig,
294 ) -> DatabaseResult<Self> {
295 Ok(Self {
296 on_chain: Database::rocksdb_temp(state_rewind_policy, database_config)?,
297 off_chain: Database::rocksdb_temp(state_rewind_policy, database_config)?,
298 relayer: Default::default(),
299 gas_price: Default::default(),
300 compression: Default::default(),
301 #[cfg(feature = "rpc")]
302 block_aggregation_storage: Default::default(),
303 })
304 }
305
306 pub fn from_config(config: &CombinedDatabaseConfig) -> DatabaseResult<Self> {
307 let combined_database = match config.database_type {
308 #[cfg(feature = "rocksdb")]
309 DbType::RocksDb => {
310 if config.database_path.as_os_str().is_empty() {
312 tracing::warn!(
313 "No RocksDB path configured, initializing database with a tmp directory"
314 );
315 CombinedDatabase::temp_database_with_state_rewind_policy(
316 config.state_rewind_policy,
317 config.database_config,
318 )?
319 } else {
320 tracing::info!(
321 "Opening database {:?} with cache size \"{:?}\" and state rewind policy \"{:?}\"",
322 config.database_path,
323 config.database_config.cache_capacity,
324 config.state_rewind_policy,
325 );
326 CombinedDatabase::open(
327 &config.database_path,
328 config.state_rewind_policy,
329 config.database_config,
330 )?
331 }
332 }
333 DbType::InMemory => CombinedDatabase::in_memory(),
334 #[cfg(not(feature = "rocksdb"))]
335 _ => CombinedDatabase::in_memory(),
336 };
337
338 Ok(combined_database)
339 }
340
341 pub fn in_memory() -> Self {
342 Self::new(
343 Database::in_memory(),
344 Database::in_memory(),
345 Database::in_memory(),
346 Database::in_memory(),
347 Database::in_memory(),
348 #[cfg(feature = "rpc")]
349 Database::in_memory(),
350 )
351 }
352
353 pub fn check_version(&self) -> StorageResult<()> {
354 self.on_chain.check_version()?;
355 self.off_chain.check_version()?;
356 self.relayer.check_version()?;
357 self.gas_price.check_version()?;
358 self.compression.check_version()?;
359 #[cfg(feature = "rpc")]
360 self.block_aggregation_storage.check_version()?;
361 Ok(())
362 }
363
364 pub fn on_chain(&self) -> &Database<OnChain> {
365 &self.on_chain
366 }
367
368 pub fn compression(&self) -> &Database<CompressionDatabase> {
369 &self.compression
370 }
371
372 #[cfg(feature = "rpc")]
373 pub fn block_aggregation_storage(&self) -> &Database<BlockAggregatorDatabase> {
374 &self.block_aggregation_storage
375 }
376
377 #[cfg(feature = "rpc")]
378 pub fn block_aggregation_storage_mut(
379 &mut self,
380 ) -> &mut Database<BlockAggregatorDatabase> {
381 &mut self.block_aggregation_storage
382 }
383
384 #[cfg(any(feature = "test-helpers", test))]
385 pub fn on_chain_mut(&mut self) -> &mut Database<OnChain> {
386 &mut self.on_chain
387 }
388
389 pub fn off_chain(&self) -> &Database<OffChain> {
390 &self.off_chain
391 }
392
393 #[cfg(any(feature = "test-helpers", test))]
394 pub fn off_chain_mut(&mut self) -> &mut Database<OffChain> {
395 &mut self.off_chain
396 }
397
398 pub fn relayer(&self) -> &Database<Relayer> {
399 &self.relayer
400 }
401
402 #[cfg(any(feature = "test-helpers", test))]
403 pub fn relayer_mut(&mut self) -> &mut Database<Relayer> {
404 &mut self.relayer
405 }
406
407 pub fn gas_price(&self) -> &Database<GasPriceDatabase> {
408 &self.gas_price
409 }
410
411 #[cfg(any(feature = "test-helpers", test))]
412 pub fn gas_price_mut(&mut self) -> &mut Database<GasPriceDatabase> {
413 &mut self.gas_price
414 }
415
416 #[cfg(feature = "test-helpers")]
417 pub fn read_state_config(&self) -> StorageResult<StateConfig> {
418 use fuel_core_chain_config::AddTable;
419 use fuel_core_producer::ports::BlockProducerDatabase;
420 use fuel_core_storage::transactional::AtomicView;
421 use fuel_core_types::fuel_vm::BlobData;
422 use itertools::Itertools;
423 let mut builder = StateConfigBuilder::default();
424
425 macro_rules! add_tables {
426 ($($table: ty),*) => {
427 $(
428 let table = self
429 .on_chain()
430 .entries::<$table>(None, fuel_core_storage::iter::IterDirection::Forward)
431 .try_collect()?;
432 builder.add(table);
433 )*
434 };
435 }
436
437 add_tables!(
438 Coins,
439 Messages,
440 BlobData,
441 ContractsAssets,
442 ContractsState,
443 ContractsRawCode,
444 ContractsLatestUtxo
445 );
446
447 let view = self.on_chain().latest_view()?;
448 let latest_block = view.latest_block()?;
449 let blocks_root =
450 view.block_header_merkle_root(latest_block.header().height())?;
451 let state_config =
452 builder.build(Some(fuel_core_chain_config::LastBlockConfig::from_header(
453 latest_block.header(),
454 blocks_root,
455 )))?;
456
457 Ok(state_config)
458 }
459
460 pub fn rollback_to<S>(
462 &mut self,
463 target_block_height: BlockHeight,
464 shutdown_listener: &mut S,
465 ) -> anyhow::Result<()>
466 where
467 S: ShutdownListener,
468 {
469 while !shutdown_listener.is_cancelled() {
470 let on_chain_height = self
471 .on_chain()
472 .latest_height_from_metadata()?
473 .ok_or(anyhow::anyhow!("on-chain database doesn't have height"))?;
474
475 let off_chain_height = self
476 .off_chain()
477 .latest_height_from_metadata()?
478 .ok_or(anyhow::anyhow!("off-chain database doesn't have height"))?;
479
480 let gas_price_chain_height =
481 self.gas_price().latest_height_from_metadata()?;
482 let gas_price_rolled_back = is_equal_or_less_than_or_none(
483 gas_price_chain_height,
484 target_block_height,
485 );
486
487 let compression_db_height =
488 self.compression().latest_height_from_metadata()?;
489 let compression_db_rolled_back =
490 is_equal_or_less_than_or_none(compression_db_height, target_block_height);
491
492 #[cfg(feature = "rpc")]
493 {
494 let block_aggregation_storage_height = self
495 .block_aggregation_storage()
496 .latest_height_from_metadata()?;
497 let block_aggregation_storage_rolled_back = is_equal_or_less_than_or_none(
498 block_aggregation_storage_height,
499 target_block_height,
500 );
501
502 if on_chain_height == target_block_height
503 && off_chain_height == target_block_height
504 && gas_price_rolled_back
505 && compression_db_rolled_back
506 && block_aggregation_storage_rolled_back
507 {
508 break;
509 }
510 }
511
512 #[cfg(not(feature = "rpc"))]
513 {
514 if on_chain_height == target_block_height
515 && off_chain_height == target_block_height
516 && gas_price_rolled_back
517 && compression_db_rolled_back
518 {
519 break;
520 }
521 }
522
523 if on_chain_height < target_block_height {
524 return Err(anyhow::anyhow!(
525 "on-chain database height({on_chain_height}) \
526 is less than target height({target_block_height})"
527 ));
528 }
529
530 if off_chain_height < target_block_height {
531 return Err(anyhow::anyhow!(
532 "off-chain database height({off_chain_height}) \
533 is less than target height({target_block_height})"
534 ));
535 }
536
537 if let Some(gas_price_chain_height) = gas_price_chain_height
538 && gas_price_chain_height < target_block_height
539 {
540 return Err(anyhow::anyhow!(
541 "gas-price database height({gas_price_chain_height}) \
542 is less than target height({target_block_height})"
543 ));
544 }
545
546 if let Some(compression_db_height) = compression_db_height
547 && compression_db_height < target_block_height
548 {
549 return Err(anyhow::anyhow!(
550 "compression database height({compression_db_height}) \
551 is less than target height({target_block_height})"
552 ));
553 }
554
555 if on_chain_height > target_block_height {
556 self.on_chain().rollback_last_block()?;
557 }
558
559 if off_chain_height > target_block_height {
560 self.off_chain().rollback_last_block()?;
561 }
562
563 if let Some(gas_price_chain_height) = gas_price_chain_height
564 && gas_price_chain_height > target_block_height
565 {
566 self.gas_price().rollback_last_block()?;
567 }
568
569 if let Some(compression_db_height) = compression_db_height
570 && compression_db_height > target_block_height
571 {
572 self.compression().rollback_last_block()?;
573 }
574
575 #[cfg(feature = "rpc")]
576 {
577 let block_aggregation_storage_height = self
578 .block_aggregation_storage()
579 .latest_height_from_metadata()?;
580
581 if let Some(block_aggregation_storage_height) =
582 block_aggregation_storage_height
583 && block_aggregation_storage_height > target_block_height
584 {
585 self.block_aggregation_storage().rollback_last_block()?;
586 }
587 }
588 }
589
590 if shutdown_listener.is_cancelled() {
591 return Err(anyhow::anyhow!(
592 "Stop the rollback due to shutdown signal received"
593 ));
594 }
595
596 Ok(())
597 }
598
599 pub fn rollback_relayer_to<S>(
601 &self,
602 target_da_height: DaBlockHeight,
603 shutdown_listener: &mut S,
604 ) -> anyhow::Result<()>
605 where
606 S: ShutdownListener,
607 {
608 while !shutdown_listener.is_cancelled() {
609 let relayer_db_height = self.relayer().latest_height_from_metadata()?;
610 let relayer_db_rolled_back =
611 is_equal_or_none(relayer_db_height, target_da_height);
612
613 if relayer_db_rolled_back {
614 break;
615 }
616
617 if let Some(relayer_db_height) = relayer_db_height
618 && relayer_db_height < target_da_height
619 {
620 return Err(anyhow::anyhow!(
621 "relayer database height({relayer_db_height}) \
622 is less than target height({target_da_height})"
623 ));
624 }
625
626 if let Some(relayer_db_height) = relayer_db_height
627 && relayer_db_height > target_da_height
628 {
629 self.relayer().rollback_last_block()?;
630 }
631 }
632
633 if shutdown_listener.is_cancelled() {
634 return Err(anyhow::anyhow!(
635 "Stop the rollback due to shutdown signal received"
636 ));
637 }
638
639 Ok(())
640 }
641
642 pub fn sync_aux_db_heights<S>(&self, shutdown_listener: &mut S) -> anyhow::Result<()>
649 where
650 S: ShutdownListener,
651 {
652 while !shutdown_listener.is_cancelled() {
653 let on_chain_height = match self.on_chain().latest_height_from_metadata()? {
654 Some(height) => height,
655 None => break, };
657
658 let off_chain_height = self.off_chain().latest_height_from_metadata()?;
659 let gas_price_height = self.gas_price().latest_height_from_metadata()?;
660
661 if let Some(off_height) = off_chain_height
663 && off_height > on_chain_height
664 {
665 self.off_chain().rollback_last_block()?;
666 }
667
668 if let Some(gas_height) = gas_price_height
670 && gas_height > on_chain_height
671 {
672 self.gas_price().rollback_last_block()?;
673 }
674
675 if off_chain_height.is_none_or(|h| h <= on_chain_height)
677 && gas_price_height.is_none_or(|h| h <= on_chain_height)
678 {
679 break;
680 }
681 }
682
683 Ok(())
684 }
685
686 pub fn shutdown(self) {
687 self.on_chain.shutdown();
688 self.off_chain.shutdown();
689 self.relayer.shutdown();
690 self.gas_price.shutdown();
691 self.compression.shutdown();
692 #[cfg(feature = "rpc")]
693 self.block_aggregation_storage.shutdown();
694 }
695}
696
697pub trait ShutdownListener {
699 fn is_cancelled(&self) -> bool;
701}
702
703#[derive(Default, Clone)]
706pub struct CombinedGenesisDatabase {
707 pub on_chain: GenesisDatabase<OnChain>,
708 pub off_chain: GenesisDatabase<OffChain>,
709}
710
711impl CombinedGenesisDatabase {
712 pub fn on_chain(&self) -> &GenesisDatabase<OnChain> {
713 &self.on_chain
714 }
715
716 pub fn off_chain(&self) -> &GenesisDatabase<OffChain> {
717 &self.off_chain
718 }
719}
720
721fn is_equal_or_none<T: PartialEq>(maybe_left: Option<T>, right: T) -> bool {
722 maybe_left.map(|left| left == right).unwrap_or(true)
723}
724
725fn is_equal_or_less_than_or_none<T: PartialOrd>(maybe_left: Option<T>, right: T) -> bool {
726 maybe_left.map(|left| left <= right).unwrap_or(true)
727}
728
729#[allow(non_snake_case)]
730#[cfg(feature = "backup")]
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use fuel_core_storage::{
735 StorageAsMut,
736 StorageAsRef,
737 };
738 use fuel_core_types::{
739 entities::coins::coin::CompressedCoin,
740 fuel_tx::UtxoId,
741 };
742 use tempfile::TempDir;
743
744 #[test]
745 fn backup_and_restore__works_correctly__happy_path() {
746 let db_dir = TempDir::new().unwrap();
748 let mut combined_db = CombinedDatabase::open(
749 db_dir.path(),
750 StateRewindPolicy::NoRewind,
751 DatabaseConfig::config_for_tests(),
752 )
753 .unwrap();
754 let key = UtxoId::new(Default::default(), Default::default());
755 let expected_value = CompressedCoin::default();
756
757 let on_chain_db = combined_db.on_chain_mut();
758 on_chain_db
759 .storage_as_mut::<Coins>()
760 .insert(&key, &expected_value)
761 .unwrap();
762 drop(combined_db);
763
764 let backup_dir = TempDir::new().unwrap();
766 CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();
767
768 let restore_dir = TempDir::new().unwrap();
770 CombinedDatabase::restore(restore_dir.path(), backup_dir.path()).unwrap();
771 let restored_db = CombinedDatabase::open(
772 restore_dir.path(),
773 StateRewindPolicy::NoRewind,
774 DatabaseConfig::config_for_tests(),
775 )
776 .unwrap();
777
778 let restored_on_chain_db = restored_db.on_chain();
779 let restored_value = restored_on_chain_db
780 .storage::<Coins>()
781 .get(&key)
782 .unwrap()
783 .unwrap()
784 .into_owned();
785 assert_eq!(expected_value, restored_value);
786
787 std::fs::remove_dir_all(db_dir.path()).unwrap();
789 std::fs::remove_dir_all(backup_dir.path()).unwrap();
790 std::fs::remove_dir_all(restore_dir.path()).unwrap();
791 }
792
793 #[test]
794 fn backup__when_backup_fails_it_should_not_leave_any_residue() {
795 use std::os::unix::fs::PermissionsExt;
796
797 let db_dir = TempDir::new().unwrap();
799 let mut combined_db = CombinedDatabase::open(
800 db_dir.path(),
801 StateRewindPolicy::NoRewind,
802 DatabaseConfig::config_for_tests(),
803 )
804 .unwrap();
805 let key = UtxoId::new(Default::default(), Default::default());
806 let expected_value = CompressedCoin::default();
807
808 let on_chain_db = combined_db.on_chain_mut();
809 on_chain_db
810 .storage_as_mut::<Coins>()
811 .insert(&key, &expected_value)
812 .unwrap();
813 drop(combined_db);
814
815 std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o030))
818 .unwrap();
819 let backup_dir = TempDir::new().unwrap();
820
821 CombinedDatabase::backup(db_dir.path(), backup_dir.path())
823 .expect_err("Backup should fail");
824 let backup_dir_contents = std::fs::read_dir(backup_dir.path()).unwrap();
825 assert_eq!(backup_dir_contents.count(), 0);
826
827 std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o770))
829 .unwrap();
830 std::fs::remove_dir_all(db_dir.path()).unwrap();
831 std::fs::remove_dir_all(backup_dir.path()).unwrap();
832 }
833
834 #[test]
835 fn restore__when_restore_fails_it_should_not_leave_any_residue() {
836 use std::os::unix::fs::PermissionsExt;
837
838 let db_dir = TempDir::new().unwrap();
840 let mut combined_db = CombinedDatabase::open(
841 db_dir.path(),
842 StateRewindPolicy::NoRewind,
843 DatabaseConfig::config_for_tests(),
844 )
845 .unwrap();
846 let key = UtxoId::new(Default::default(), Default::default());
847 let expected_value = CompressedCoin::default();
848
849 let on_chain_db = combined_db.on_chain_mut();
850 on_chain_db
851 .storage_as_mut::<Coins>()
852 .insert(&key, &expected_value)
853 .unwrap();
854 drop(combined_db);
855
856 let backup_dir = TempDir::new().unwrap();
857 CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();
858
859 std::fs::set_permissions(
862 backup_dir.path(),
863 std::fs::Permissions::from_mode(0o030),
864 )
865 .unwrap();
866 let restore_dir = TempDir::new().unwrap();
867
868 CombinedDatabase::restore(restore_dir.path(), backup_dir.path())
870 .expect_err("Restore should fail");
871 let restore_dir_contents = std::fs::read_dir(restore_dir.path()).unwrap();
872 assert_eq!(restore_dir_contents.count(), 0);
873
874 std::fs::set_permissions(
876 backup_dir.path(),
877 std::fs::Permissions::from_mode(0o770),
878 )
879 .unwrap();
880 std::fs::remove_dir_all(db_dir.path()).unwrap();
881 std::fs::remove_dir_all(backup_dir.path()).unwrap();
882 std::fs::remove_dir_all(restore_dir.path()).unwrap();
883 }
884
885 #[test]
886 fn backup__should_be_successful_if_db_is_already_opened() {
887 let db_dir = TempDir::new().unwrap();
889 let mut combined_db = CombinedDatabase::open(
890 db_dir.path(),
891 StateRewindPolicy::NoRewind,
892 DatabaseConfig::config_for_tests(),
893 )
894 .unwrap();
895 let key = UtxoId::new(Default::default(), Default::default());
896 let expected_value = CompressedCoin::default();
897
898 let on_chain_db = combined_db.on_chain_mut();
899 on_chain_db
900 .storage_as_mut::<Coins>()
901 .insert(&key, &expected_value)
902 .unwrap();
903
904 let backup_dir = TempDir::new().unwrap();
906 let res = CombinedDatabase::backup(db_dir.path(), backup_dir.path());
910 assert!(res.is_ok());
911
912 std::fs::remove_dir_all(db_dir.path()).unwrap();
914 std::fs::remove_dir_all(backup_dir.path()).unwrap();
915 }
916}