ethexe-db 2.0.0-pre.1

Database abstractions and implementations for ethexe
// Copyright (C) Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

//! # ethexe-db
//!
//! Storage abstraction and implementation layer for the ethexe node. Defines the low-level
//! [`CASDatabase`] and [`KVDatabase`] backend traits, provides two concrete backends
//! ([`RocksDatabase`] for persistence and [`MemDb`] for tests), and composes them into the typed,
//! domain-aware [`Database`] that every ethexe service reads and writes.
//!
//! ## Role in the stack
//!
//! `ethexe-service` owns the single [`Database`] instance and passes it to every subsystem
//! (`ethexe-observer`, `ethexe-compute`, `ethexe-processor`, `ethexe-consensus`, `ethexe-rpc-server`).
//! The storage-role trait *definitions* live in `ethexe-common::db`; [`Database`] implements them
//! here, plus the runtime `Storage` trait from `ethexe-runtime-common`.
//!
//! ## Public API
//!
//! - [`CASDatabase`] — Content-addressable backend trait (`write(&[u8]) -> H256`, `read`, `contains`)
//! - [`KVDatabase`] — Key-value backend trait (`get`/`put`/`contains`/`iter_prefix`/`take`)
//! - [`Database`] — Typed domain database; primary handle held by services
//! - [`RawDatabase`] — Un-typed `{ kv, cas }` pairing used during construction
//! - [`RocksDatabase`] — Persistent backend (`RocksDatabase::open(path)`)
//! - [`MemDb`] — In-memory backend for tests
//! - [`hash`] — Blake2 hash helper; matches the key returned by `CASDatabase::write`
//! - [`initialize_db`] — Versioned DB bring-up and migration entry point
//! - [`dump`] — State dump for re-genesis
//! - [`iterator`] / [`visitor`] — Graph traversal over the content-addressed state
//! - [`verifier`] — DB integrity verification backing `ethexe check`
//!
//! Versioned initialization and migrations go through [`initialize_db`], [`GenesisInitializer`],
//! [`InitConfig`], and [`VERSION`]. Under `feature = "mock"`, `create_initialized_empty_memory_db`
//! constructs a fully-initialized [`MemDb`]-backed [`Database`] for integration tests.
//!
//! ## Invariants
//!
//! - [`Database::try_from_raw`](Database::try_from_raw) rejects any backend whose stored version
//!   does not match [`VERSION`]; a mismatch is a hard error requiring migration.
//! - `KVDatabase::take` is `unsafe`: removing a key without re-insertion may cause permanent
//!   data loss.
//! - Both backends are `Send + Sync`; concurrent reads and writes from multiple threads are safe.
//! - CAS round-trip: `write(data)` returns `hash(data)`, and `read(that_hash)` always returns the
//!   same bytes.

use gprimitives::H256;

mod database;
pub mod dump;
pub mod iterator;
mod mem;
mod migrations;
mod overlay;
mod rocks;
pub mod verifier;
pub mod visitor;

pub use database::{Database, RawDatabase};
pub use mem::MemDb;
pub use migrations::{
    CodeProcessingFuture, GenesisInitializer, InitConfig, LATEST_VERSION as VERSION, initialize_db,
};
pub use rocks::RocksDatabase;

#[cfg(feature = "mock")]
pub use migrations::create_initialized_empty_memory_db;

pub fn hash(data: &[u8]) -> H256 {
    gear_core::utils::hash(data).into()
}

/// Content-addressable storage database.
pub trait CASDatabase: Send + Sync {
    /// Clone ref to database instance.
    fn clone_boxed(&self) -> Box<dyn CASDatabase>;

    /// Read data by hash.
    fn read(&self, hash: H256) -> Option<Vec<u8>>;

    /// Check if data exists by key.
    fn contains(&self, hash: H256) -> bool;

    /// Write data, returns data hash.
    fn write(&self, data: &[u8]) -> H256;
}

/// Key-value database.
pub trait KVDatabase: Send + Sync {
    /// Clone ref to key-value database instance.
    fn clone_boxed(&self) -> Box<dyn KVDatabase>;

    /// Get value by key.
    fn get(&self, key: &[u8]) -> Option<Vec<u8>>;

    /// Take (get and remove) value by key.
    ///
    /// # Safety
    /// This method is unsafe because it may lead to data loss if used improperly.
    /// Must be used with caution.
    unsafe fn take(&self, key: &[u8]) -> Option<Vec<u8>>;

    /// Remove value by key without reading it.
    ///
    /// # Safety
    /// This method is unsafe because it may lead to data loss if used improperly.
    /// Must be used with caution.
    unsafe fn delete(&self, key: &[u8]);

    /// Check if data exists by key.
    fn contains(&self, key: &[u8]) -> bool;

    /// Put (insert) value by key.
    fn put(&self, key: &[u8], data: Vec<u8>);

    fn iter_prefix<'a>(
        &'a self,
        prefix: &'a [u8],
    ) -> Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a>;

    /// Force compaction of the `[start, end]` key range so deleted entries
    /// are physically dropped from disk. Blocking and I/O-heavy; no-op for
    /// backends without compaction.
    fn compact_range(&self, _start: &[u8], _end: &[u8]) {}

    fn is_empty(&self) -> bool;
}

#[cfg(test)]
mod tests {
    use std::{collections::BTreeSet, thread};

    use crate::{CASDatabase, KVDatabase};

    fn to_big_vec(x: u32) -> Vec<u8> {
        let bytes = x.to_le_bytes();
        bytes
            .iter()
            .cycle()
            .take(1024 * 1024)
            .copied()
            .collect::<Vec<_>>()
    }

    pub fn is_cloneable<DB: Clone>(db: DB) {
        let _ = db.clone();
    }

    pub fn cas_read_write<DB: CASDatabase>(db: DB) {
        let data = b"Hello, world!";
        let hash = db.write(data);
        assert_eq!(db.read(hash), Some(data.to_vec()));
    }

    pub fn kv_read_write<DB: KVDatabase>(db: DB) {
        let key = b"key";
        let data = b"value".to_vec();
        db.put(key, data.clone());
        assert_eq!(db.get(key.as_slice()), Some(data));
    }

    pub fn kv_iter_prefix<DB: KVDatabase>(db: DB) {
        let testcase = |prefix: &str, expectations: &[(&str, &str)]| {
            let actual: BTreeSet<_> = db.iter_prefix(prefix.as_bytes()).collect();
            let expected: BTreeSet<_> = expectations
                .iter()
                .map(|(k, v)| (k.as_bytes().to_vec(), v.as_bytes().to_vec()))
                .collect();
            assert_eq!(actual, expected);
        };

        db.put(b"prefix_foo", b"hello".to_vec());
        db.put(b"prefix_bar", b"world".to_vec());

        testcase(
            "prefix_",
            &[("prefix_foo", "hello"), ("prefix_bar", "world")],
        );

        testcase("", &[("prefix_foo", "hello"), ("prefix_bar", "world")]);

        testcase("0", &[]);

        testcase("prefix_foobar", &[]);

        testcase("prefix_foo", &[("prefix_foo", "hello")]);

        testcase("prefix_bar", &[("prefix_bar", "world")]);
    }

    pub fn cas_multi_thread<DB: CASDatabase>(db: DB) {
        let amount = 10;

        let db_clone = CASDatabase::clone_boxed(&db);
        let handler1 = thread::spawn(move || {
            for x in 0u32..amount {
                db_clone.write(to_big_vec(x).as_slice());
            }
        });

        let db_clone = CASDatabase::clone_boxed(&db);
        let handler2 = thread::spawn(move || {
            for x in amount..amount * 2 {
                db_clone.write(to_big_vec(x).as_slice());
            }
        });

        handler1.join().unwrap();
        handler2.join().unwrap();

        for x in 0u32..amount * 2 {
            let expected = to_big_vec(x);
            let data = db.read(crate::hash(expected.as_slice()));
            assert_eq!(data, Some(expected));
        }
    }

    pub fn kv_multi_thread<DB: KVDatabase>(db: DB) {
        let amount = 10;

        let db_clone = KVDatabase::clone_boxed(&db);
        let handler1 = thread::spawn(move || {
            for x in 0u32..amount {
                db_clone.put(x.to_le_bytes().as_slice(), to_big_vec(x));
            }
        });

        let db_clone = KVDatabase::clone_boxed(&db);
        let handler2 = thread::spawn(move || {
            for x in amount..amount * 2 {
                db_clone.put(x.to_le_bytes().as_slice(), to_big_vec(x));
            }
        });

        handler1.join().unwrap();
        handler2.join().unwrap();

        for x in 0u32..amount * 2 {
            let expected = to_big_vec(x);
            let data = db.get(x.to_le_bytes().as_slice());
            assert_eq!(data, Some(expected));
        }
    }
}