fuel_core/
combined_database.rs

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