Skip to main content

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
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/// A database that combines the on-chain, off-chain and relayer databases into one entity.
59#[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        // we don't return a CombinedDatabase here
171        // because the consumer can use any db config while opening it
172        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        // Split the fds in equitable manner between the databases
221
222        let max_fds = match database_config.max_fds {
223            -1 => -1,
224            _ => database_config.max_fds.saturating_div(4),
225        };
226
227        // TODO: Use different cache sizes for different databases
228        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    /// A test-only temporary rocksdb database with given rewind policy.
290    #[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                // use a default tmp rocksdb if no path is provided
311                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    /// Rollbacks the state of the blockchain to a specific block height.
461    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    /// Rollbacks the state of the relayer to a specific block height.
600    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    /// This function is fundamentally different from `rollback_to` in that it
643    /// will rollback the off-chain/gas-price databases if they are ahead of the
644    /// on-chain database. If they don't have a height or are behind the on-chain
645    /// we leave it to the caller to decide how to bring them up to date.
646    /// We don't rollback the on-chain database as it is the source of truth.
647    /// The target height of the rollback is the latest height of the on-chain database.
648    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, // Exit loop if on-chain height is None
656            };
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            // Handle off-chain rollback if necessary
662            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            // Handle gas price rollback if necessary
669            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 both off-chain and gas price heights are synced, break
676            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
697/// A trait for listening to shutdown signals.
698pub trait ShutdownListener {
699    /// Returns true if the shutdown signal has been received.
700    fn is_cancelled(&self) -> bool;
701}
702
703/// A genesis database that combines the on-chain, off-chain and relayer
704/// genesis databases into one entity.
705#[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        // given
747        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        // when
765        let backup_dir = TempDir::new().unwrap();
766        CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();
767
768        // then
769        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        // cleanup
788        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        // given
798        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        // when
816        // we set the permissions of db_dir to not allow reading
817        std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o030))
818            .unwrap();
819        let backup_dir = TempDir::new().unwrap();
820
821        // then
822        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        // cleanup
828        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        // given
839        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        // when
860        // we set the permissions of backup_dir to not allow reading
861        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        // then
869        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        // cleanup
875        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        // given
888        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        // when
905        let backup_dir = TempDir::new().unwrap();
906        // no drop for combined_db
907
908        // then
909        let res = CombinedDatabase::backup(db_dir.path(), backup_dir.path());
910        assert!(res.is_ok());
911
912        // cleanup
913        std::fs::remove_dir_all(db_dir.path()).unwrap();
914        std::fs::remove_dir_all(backup_dir.path()).unwrap();
915    }
916}