From c7c2631457dcb040f9e6fd89ee9a5df422443b10 Mon Sep 17 00:00:00 2001 From: "hinto.janai" <hinto.janai@protonmail.com> Date: Mon, 27 May 2024 19:56:13 -0400 Subject: [PATCH] `database/ -> `storage/` --- {database => storage}/README.md | 0 .../cuprate-database}/Cargo.toml | 0 storage/cuprate-database/README.md | 598 ++++++++++++++++++ .../src/backend/heed/database.rs | 0 .../cuprate-database}/src/backend/heed/env.rs | 0 .../src/backend/heed/error.rs | 0 .../cuprate-database}/src/backend/heed/mod.rs | 0 .../src/backend/heed/storable.rs | 0 .../src/backend/heed/transaction.rs | 0 .../src/backend/heed/types.rs | 0 .../cuprate-database}/src/backend/mod.rs | 0 .../src/backend/redb/database.rs | 0 .../cuprate-database}/src/backend/redb/env.rs | 0 .../src/backend/redb/error.rs | 0 .../cuprate-database}/src/backend/redb/mod.rs | 0 .../src/backend/redb/storable.rs | 0 .../src/backend/redb/transaction.rs | 0 .../src/backend/redb/types.rs | 0 .../cuprate-database}/src/backend/tests.rs | 0 .../cuprate-database}/src/config/backend.rs | 0 .../cuprate-database}/src/config/config.rs | 0 .../cuprate-database}/src/config/mod.rs | 0 .../src/config/reader_threads.rs | 0 .../cuprate-database}/src/config/sync_mode.rs | 0 .../cuprate-database}/src/constants.rs | 0 .../cuprate-database}/src/database.rs | 0 .../cuprate-database}/src/env.rs | 0 .../cuprate-database}/src/error.rs | 0 .../cuprate-database}/src/free.rs | 0 .../cuprate-database}/src/key.rs | 0 .../cuprate-database}/src/lib.rs | 0 .../cuprate-database}/src/ops/block.rs | 0 .../cuprate-database}/src/ops/blockchain.rs | 0 .../cuprate-database}/src/ops/key_image.rs | 0 .../cuprate-database}/src/ops/macros.rs | 0 .../cuprate-database}/src/ops/mod.rs | 0 .../cuprate-database}/src/ops/output.rs | 0 .../cuprate-database}/src/ops/property.rs | 0 .../cuprate-database}/src/ops/tx.rs | 0 .../cuprate-database}/src/resize.rs | 0 .../cuprate-database}/src/service/free.rs | 0 .../cuprate-database}/src/service/mod.rs | 0 .../cuprate-database}/src/service/read.rs | 0 .../cuprate-database}/src/service/tests.rs | 0 .../cuprate-database}/src/service/types.rs | 0 .../cuprate-database}/src/service/write.rs | 0 .../cuprate-database}/src/storable.rs | 0 .../cuprate-database}/src/table.rs | 0 .../cuprate-database}/src/tables.rs | 0 .../cuprate-database}/src/tests.rs | 0 .../cuprate-database}/src/transaction.rs | 0 .../cuprate-database}/src/types.rs | 0 .../cuprate-database}/src/unsafe_sendable.rs | 0 storage/database/Cargo.toml | 59 ++ storage/database/README.md | 598 ++++++++++++++++++ storage/database/src/backend/heed/database.rs | 261 ++++++++ storage/database/src/backend/heed/env.rs | 347 ++++++++++ storage/database/src/backend/heed/error.rs | 152 +++++ storage/database/src/backend/heed/mod.rs | 10 + storage/database/src/backend/heed/storable.rs | 122 ++++ .../database/src/backend/heed/transaction.rs | 41 ++ storage/database/src/backend/heed/types.rs | 8 + storage/database/src/backend/mod.rs | 16 + storage/database/src/backend/redb/database.rs | 213 +++++++ storage/database/src/backend/redb/env.rs | 226 +++++++ storage/database/src/backend/redb/error.rs | 172 +++++ storage/database/src/backend/redb/mod.rs | 9 + storage/database/src/backend/redb/storable.rs | 221 +++++++ .../database/src/backend/redb/transaction.rs | 38 ++ storage/database/src/backend/redb/types.rs | 11 + storage/database/src/backend/tests.rs | 550 ++++++++++++++++ storage/database/src/config/backend.rs | 31 + storage/database/src/config/config.rs | 237 +++++++ storage/database/src/config/mod.rs | 47 ++ storage/database/src/config/reader_threads.rs | 189 ++++++ storage/database/src/config/sync_mode.rs | 135 ++++ storage/database/src/constants.rs | 86 +++ storage/database/src/database.rs | 216 +++++++ storage/database/src/env.rs | 286 +++++++++ storage/database/src/error.rs | 94 +++ storage/database/src/free.rs | 11 + storage/database/src/key.rs | 58 ++ storage/database/src/lib.rs | 301 +++++++++ storage/database/src/ops/block.rs | 472 ++++++++++++++ storage/database/src/ops/blockchain.rs | 182 ++++++ storage/database/src/ops/key_image.rs | 127 ++++ storage/database/src/ops/macros.rs | 33 + storage/database/src/ops/mod.rs | 110 ++++ storage/database/src/ops/output.rs | 371 +++++++++++ storage/database/src/ops/property.rs | 39 ++ storage/database/src/ops/tx.rs | 434 +++++++++++++ storage/database/src/resize.rs | 307 +++++++++ storage/database/src/service/free.rs | 40 ++ storage/database/src/service/mod.rs | 130 ++++ storage/database/src/service/read.rs | 493 +++++++++++++++ storage/database/src/service/tests.rs | 377 +++++++++++ storage/database/src/service/types.rs | 31 + storage/database/src/service/write.rs | 245 +++++++ storage/database/src/storable.rs | 347 ++++++++++ storage/database/src/table.rs | 31 + storage/database/src/tables.rs | 476 ++++++++++++++ storage/database/src/tests.rs | 85 +++ storage/database/src/transaction.rs | 43 ++ storage/database/src/types.rs | 324 ++++++++++ storage/database/src/unsafe_sendable.rs | 85 +++ 105 files changed, 10125 insertions(+) rename {database => storage}/README.md (100%) rename {database => storage/cuprate-database}/Cargo.toml (100%) create mode 100644 storage/cuprate-database/README.md rename {database => storage/cuprate-database}/src/backend/heed/database.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/env.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/error.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/mod.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/storable.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/transaction.rs (100%) rename {database => storage/cuprate-database}/src/backend/heed/types.rs (100%) rename {database => storage/cuprate-database}/src/backend/mod.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/database.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/env.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/error.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/mod.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/storable.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/transaction.rs (100%) rename {database => storage/cuprate-database}/src/backend/redb/types.rs (100%) rename {database => storage/cuprate-database}/src/backend/tests.rs (100%) rename {database => storage/cuprate-database}/src/config/backend.rs (100%) rename {database => storage/cuprate-database}/src/config/config.rs (100%) rename {database => storage/cuprate-database}/src/config/mod.rs (100%) rename {database => storage/cuprate-database}/src/config/reader_threads.rs (100%) rename {database => storage/cuprate-database}/src/config/sync_mode.rs (100%) rename {database => storage/cuprate-database}/src/constants.rs (100%) rename {database => storage/cuprate-database}/src/database.rs (100%) rename {database => storage/cuprate-database}/src/env.rs (100%) rename {database => storage/cuprate-database}/src/error.rs (100%) rename {database => storage/cuprate-database}/src/free.rs (100%) rename {database => storage/cuprate-database}/src/key.rs (100%) rename {database => storage/cuprate-database}/src/lib.rs (100%) rename {database => storage/cuprate-database}/src/ops/block.rs (100%) rename {database => storage/cuprate-database}/src/ops/blockchain.rs (100%) rename {database => storage/cuprate-database}/src/ops/key_image.rs (100%) rename {database => storage/cuprate-database}/src/ops/macros.rs (100%) rename {database => storage/cuprate-database}/src/ops/mod.rs (100%) rename {database => storage/cuprate-database}/src/ops/output.rs (100%) rename {database => storage/cuprate-database}/src/ops/property.rs (100%) rename {database => storage/cuprate-database}/src/ops/tx.rs (100%) rename {database => storage/cuprate-database}/src/resize.rs (100%) rename {database => storage/cuprate-database}/src/service/free.rs (100%) rename {database => storage/cuprate-database}/src/service/mod.rs (100%) rename {database => storage/cuprate-database}/src/service/read.rs (100%) rename {database => storage/cuprate-database}/src/service/tests.rs (100%) rename {database => storage/cuprate-database}/src/service/types.rs (100%) rename {database => storage/cuprate-database}/src/service/write.rs (100%) rename {database => storage/cuprate-database}/src/storable.rs (100%) rename {database => storage/cuprate-database}/src/table.rs (100%) rename {database => storage/cuprate-database}/src/tables.rs (100%) rename {database => storage/cuprate-database}/src/tests.rs (100%) rename {database => storage/cuprate-database}/src/transaction.rs (100%) rename {database => storage/cuprate-database}/src/types.rs (100%) rename {database => storage/cuprate-database}/src/unsafe_sendable.rs (100%) create mode 100644 storage/database/Cargo.toml create mode 100644 storage/database/README.md create mode 100644 storage/database/src/backend/heed/database.rs create mode 100644 storage/database/src/backend/heed/env.rs create mode 100644 storage/database/src/backend/heed/error.rs create mode 100644 storage/database/src/backend/heed/mod.rs create mode 100644 storage/database/src/backend/heed/storable.rs create mode 100644 storage/database/src/backend/heed/transaction.rs create mode 100644 storage/database/src/backend/heed/types.rs create mode 100644 storage/database/src/backend/mod.rs create mode 100644 storage/database/src/backend/redb/database.rs create mode 100644 storage/database/src/backend/redb/env.rs create mode 100644 storage/database/src/backend/redb/error.rs create mode 100644 storage/database/src/backend/redb/mod.rs create mode 100644 storage/database/src/backend/redb/storable.rs create mode 100644 storage/database/src/backend/redb/transaction.rs create mode 100644 storage/database/src/backend/redb/types.rs create mode 100644 storage/database/src/backend/tests.rs create mode 100644 storage/database/src/config/backend.rs create mode 100644 storage/database/src/config/config.rs create mode 100644 storage/database/src/config/mod.rs create mode 100644 storage/database/src/config/reader_threads.rs create mode 100644 storage/database/src/config/sync_mode.rs create mode 100644 storage/database/src/constants.rs create mode 100644 storage/database/src/database.rs create mode 100644 storage/database/src/env.rs create mode 100644 storage/database/src/error.rs create mode 100644 storage/database/src/free.rs create mode 100644 storage/database/src/key.rs create mode 100644 storage/database/src/lib.rs create mode 100644 storage/database/src/ops/block.rs create mode 100644 storage/database/src/ops/blockchain.rs create mode 100644 storage/database/src/ops/key_image.rs create mode 100644 storage/database/src/ops/macros.rs create mode 100644 storage/database/src/ops/mod.rs create mode 100644 storage/database/src/ops/output.rs create mode 100644 storage/database/src/ops/property.rs create mode 100644 storage/database/src/ops/tx.rs create mode 100644 storage/database/src/resize.rs create mode 100644 storage/database/src/service/free.rs create mode 100644 storage/database/src/service/mod.rs create mode 100644 storage/database/src/service/read.rs create mode 100644 storage/database/src/service/tests.rs create mode 100644 storage/database/src/service/types.rs create mode 100644 storage/database/src/service/write.rs create mode 100644 storage/database/src/storable.rs create mode 100644 storage/database/src/table.rs create mode 100644 storage/database/src/tables.rs create mode 100644 storage/database/src/tests.rs create mode 100644 storage/database/src/transaction.rs create mode 100644 storage/database/src/types.rs create mode 100644 storage/database/src/unsafe_sendable.rs diff --git a/database/README.md b/storage/README.md similarity index 100% rename from database/README.md rename to storage/README.md diff --git a/database/Cargo.toml b/storage/cuprate-database/Cargo.toml similarity index 100% rename from database/Cargo.toml rename to storage/cuprate-database/Cargo.toml diff --git a/storage/cuprate-database/README.md b/storage/cuprate-database/README.md new file mode 100644 index 00000000..293413ac --- /dev/null +++ b/storage/cuprate-database/README.md @@ -0,0 +1,598 @@ +# Database +Cuprate's database implementation. + +- [1. Documentation](#1-documentation) +- [2. File structure](#2-file-structure) + - [2.1 `src/`](#21-src) + - [2.2 `src/backend/`](#22-srcbackend) + - [2.3 `src/config/`](#23-srcconfig) + - [2.4 `src/ops/`](#24-srcops) + - [2.5 `src/service/`](#25-srcservice) +- [3. Backends](#3-backends) + - [3.1 heed](#31-heed) + - [3.2 redb](#32-redb) + - [3.3 redb-memory](#33-redb-memory) + - [3.4 sanakirja](#34-sanakirja) + - [3.5 MDBX](#35-mdbx) +- [4. Layers](#4-layers) + - [4.1 Backend](#41-backend) + - [4.2 Trait](#42-trait) + - [4.3 ConcreteEnv](#43-concreteenv) + - [4.4 ops](#44-ops) + - [4.5 service](#45-service) +- [5. The service](#5-the-service) + - [5.1 Initialization](#51-initialization) + - [5.2 Requests](#53-requests) + - [5.3 Responses](#54-responses) + - [5.4 Thread model](#52-thread-model) + - [5.5 Shutdown](#55-shutdown) +- [6. Syncing](#6-Syncing) +- [7. Resizing](#7-resizing) +- [8. (De)serialization](#8-deserialization) +- [9. Schema](#9-schema) + - [9.1 Tables](#91-tables) + - [9.2 Multimap tables](#92-multimap-tables) +- [10. Known issues and tradeoffs](#10-known-issues-and-tradeoffs) + - [10.1 Traits abstracting backends](#101-traits-abstracting-backends) + - [10.2 Hot-swappable backends](#102-hot-swappable-backends) + - [10.3 Copying unaligned bytes](#103-copying-unaligned-bytes) + - [10.4 Endianness](#104-endianness) + - [10.5 Extra table data](#105-extra-table-data) + +--- + +## 1. Documentation +Documentation for `database/` is split into 3 locations: + +| Documentation location | Purpose | +|---------------------------|---------| +| `database/README.md` | High level design of `cuprate-database` +| `cuprate-database` | Practical usage documentation/warnings/notes/etc +| Source file `// comments` | Implementation-specific details (e.g, how many reader threads to spawn?) + +This README serves as the implementation design document. + +For actual practical usage, `cuprate-database`'s types and general usage are documented via standard Rust tooling. + +Run: +```bash +cargo doc --package cuprate-database --open +``` +at the root of the repo to open/read the documentation. + +If this documentation is too abstract, refer to any of the source files, they are heavily commented. There are many `// Regular comments` that explain more implementation specific details that aren't present here or in the docs. Use the file reference below to find what you're looking for. + +The code within `src/` is also littered with some `grep`-able comments containing some keywords: + +| Word | Meaning | +|-------------|---------| +| `INVARIANT` | This code makes an _assumption_ that must be upheld for correctness +| `SAFETY` | This `unsafe` code is okay, for `x,y,z` reasons +| `FIXME` | This code works but isn't ideal +| `HACK` | This code is a brittle workaround +| `PERF` | This code is weird for performance reasons +| `TODO` | This must be implemented; There should be 0 of these in production code +| `SOMEDAY` | This should be implemented... someday + +## 2. File structure +A quick reference of the structure of the folders & files in `cuprate-database`. + +Note that `lib.rs/mod.rs` files are purely for re-exporting/visibility/lints, and contain no code. Each sub-directory has a corresponding `mod.rs`. + +### 2.1 `src/` +The top-level `src/` files. + +| File | Purpose | +|------------------------|---------| +| `constants.rs` | General constants used throughout `cuprate-database` +| `database.rs` | Abstracted database; `trait DatabaseR{o,w}` +| `env.rs` | Abstracted database environment; `trait Env` +| `error.rs` | Database error types +| `free.rs` | General free functions (related to the database) +| `key.rs` | Abstracted database keys; `trait Key` +| `resize.rs` | Database resizing algorithms +| `storable.rs` | Data (de)serialization; `trait Storable` +| `table.rs` | Database table abstraction; `trait Table` +| `tables.rs` | All the table definitions used by `cuprate-database` +| `tests.rs` | Utilities for `cuprate_database` testing +| `transaction.rs` | Database transaction abstraction; `trait TxR{o,w}` +| `types.rs` | Database-specific types +| `unsafe_unsendable.rs` | Marker type to impl `Send` for objects not `Send` + +### 2.2 `src/backend/` +This folder contains the implementation for actual databases used as the backend for `cuprate-database`. + +Each backend has its own folder. + +| Folder/File | Purpose | +|-------------|---------| +| `heed/` | Backend using using [`heed`](https://github.com/meilisearch/heed) (LMDB) +| `redb/` | Backend using [`redb`](https://github.com/cberner/redb) +| `tests.rs` | Backend-agnostic tests + +All backends follow the same file structure: + +| File | Purpose | +|------------------|---------| +| `database.rs` | Implementation of `trait DatabaseR{o,w}` +| `env.rs` | Implementation of `trait Env` +| `error.rs` | Implementation of backend's errors to `cuprate_database`'s error types +| `storable.rs` | Compatibility layer between `cuprate_database::Storable` and backend-specific (de)serialization +| `transaction.rs` | Implementation of `trait TxR{o,w}` +| `types.rs` | Type aliases for long backend-specific types + +### 2.3 `src/config/` +This folder contains the `cupate_database::config` module; configuration options for the database. + +| File | Purpose | +|---------------------|---------| +| `config.rs` | Main database `Config` struct +| `reader_threads.rs` | Reader thread configuration for `service` thread-pool +| `sync_mode.rs` | Disk sync configuration for backends + +### 2.4 `src/ops/` +This folder contains the `cupate_database::ops` module. + +These are higher-level functions abstracted over the database, that are Monero-related. + +| File | Purpose | +|-----------------|---------| +| `block.rs` | Block related (main functions) +| `blockchain.rs` | Blockchain related (height, cumulative values, etc) +| `key_image.rs` | Key image related +| `macros.rs` | Macros specific to `ops/` +| `output.rs` | Output related +| `property.rs` | Database properties (pruned, version, etc) +| `tx.rs` | Transaction related + +### 2.5 `src/service/` +This folder contains the `cupate_database::service` module. + +The `async`hronous request/response API other Cuprate crates use instead of managing the database directly themselves. + +| File | Purpose | +|----------------|---------| +| `free.rs` | General free functions used (related to `cuprate_database::service`) +| `read.rs` | Read thread-pool definitions and logic +| `tests.rs` | Thread-pool tests and test helper functions +| `types.rs` | `cuprate_database::service`-related type aliases +| `write.rs` | Writer thread definitions and logic + +## 3. Backends +`cuprate-database`'s `trait`s allow abstracting over the actual database, such that any backend in particular could be used. + +Each database's implementation for those `trait`'s are located in its respective folder in `src/backend/${DATABASE_NAME}/`. + +### 3.1 heed +The default database used is [`heed`](https://github.com/meilisearch/heed) (LMDB). The upstream versions from [`crates.io`](https://crates.io/crates/heed) are used. `LMDB` should not need to be installed as `heed` has a build script that pulls it in automatically. + +`heed`'s filenames inside Cuprate's database folder (`~/.local/share/cuprate/database/`) are: + +| Filename | Purpose | +|------------|---------| +| `data.mdb` | Main data file +| `lock.mdb` | Database lock file + +`heed`-specific notes: +- [There is a maximum reader limit](https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1372). Other potential processes (e.g. `xmrblocks`) that are also reading the `data.mdb` file need to be accounted for +- [LMDB does not work on remote filesystem](https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/lmdb.h#L129) + +### 3.2 redb +The 2nd database backend is the 100% Rust [`redb`](https://github.com/cberner/redb). + +The upstream versions from [`crates.io`](https://crates.io/crates/redb) are used. + +`redb`'s filenames inside Cuprate's database folder (`~/.local/share/cuprate/database/`) are: + +| Filename | Purpose | +|-------------|---------| +| `data.redb` | Main data file + +<!-- TODO: document DB on remote filesystem (does redb allow this?) --> + +### 3.3 redb-memory +This backend is 100% the same as `redb`, although, it uses `redb::backend::InMemoryBackend` which is a database that completely resides in memory instead of a file. + +All other details about this should be the same as the normal `redb` backend. + +### 3.4 sanakirja +[`sanakirja`](https://docs.rs/sanakirja) was a candidate as a backend, however there were problems with maximum value sizes. + +The default maximum value size is [1012 bytes](https://docs.rs/sanakirja/1.4.1/sanakirja/trait.Storable.html) which was too small for our requirements. Using [`sanakirja::Slice`](https://docs.rs/sanakirja/1.4.1/sanakirja/union.Slice.html) and [sanakirja::UnsizedStorage](https://docs.rs/sanakirja/1.4.1/sanakirja/trait.UnsizedStorable.html) was attempted, but there were bugs found when inserting a value in-between `512..=4096` bytes. + +As such, it is not implemented. + +### 3.5 MDBX +[`MDBX`](https://erthink.github.io/libmdbx) was a candidate as a backend, however MDBX deprecated the custom key/value comparison functions, this makes it a bit trickier to implement [`9.2 Multimap tables`](#92-multimap-tables). It is also quite similar to the main backend LMDB (of which it was originally a fork of). + +As such, it is not implemented (yet). + +## 4. Layers +`cuprate_database` is logically abstracted into 5 layers, with each layer being built upon the last. + +Starting from the lowest: +1. Backend +2. Trait +3. ConcreteEnv +4. `ops` +5. `service` + +<!-- TODO: insert image here after database/ split --> + +### 4.1 Backend +This is the actual database backend implementation (or a Rust shim over one). + +Examples: +- `heed` (LMDB) +- `redb` + +`cuprate_database` itself just uses a backend, it does not implement one. + +All backends have the following attributes: +- [Embedded](https://en.wikipedia.org/wiki/Embedded_database) +- [Multiversion concurrency control](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) +- [ACID](https://en.wikipedia.org/wiki/ACID) +- Are `(key, value)` oriented and have the expected API (`get()`, `insert()`, `delete()`) +- Are table oriented (`"table_name" -> (key, value)`) +- Allows concurrent readers + +### 4.2 Trait +`cuprate_database` provides a set of `trait`s that abstract over the various database backends. + +This allows the function signatures and behavior to stay the same but allows for swapping out databases in an easier fashion. + +All common behavior of the backend's are encapsulated here and used instead of using the backend directly. + +Examples: +- [`trait Env`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/env.rs) +- [`trait {TxRo, TxRw}`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/transaction.rs) +- [`trait {DatabaseRo, DatabaseRw}`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/database.rs) + +For example, instead of calling `LMDB` or `redb`'s `get()` function directly, `DatabaseRo::get()` is called. + +### 4.3 ConcreteEnv +This is the non-generic, concrete `struct` provided by `cuprate_database` that contains all the data necessary to operate the database. The actual database backend `ConcreteEnv` will use internally depends on which backend feature is used. + +`ConcreteEnv` implements `trait Env`, which opens the door to all the other traits. + +The equivalent objects in the backends themselves are: +- [`heed::Env`](https://docs.rs/heed/0.20.0/heed/struct.Env.html) +- [`redb::Database`](https://docs.rs/redb/2.1.0/redb/struct.Database.html) + +This is the main object used when handling the database directly, although that is not strictly necessary as a user if the [`4.5 service`](#45-service) layer is used. + +### 4.4 ops +These are Monero-specific functions that use the abstracted `trait` forms of the database. + +Instead of dealing with the database directly: +- `get()` +- `delete()` + +the `ops` layer provides more abstract functions that deal with commonly used Monero operations: +- `add_block()` +- `pop_block()` + +### 4.5 service +The final layer abstracts the database completely into a [Monero-specific `async` request/response API](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/types/src/service.rs#L18-L78) using [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html). + +For more information on this layer, see the next section: [`5. The service`](#5-the-service). + +## 5. The service +The main API `cuprate_database` exposes for other crates to use is the `cuprate_database::service` module. + +This module exposes an `async` request/response API with `tower::Service`, backed by a threadpool, that allows reading/writing Monero-related data from/to the database. + +`cuprate_database::service` itself manages the database using a separate writer thread & reader thread-pool, and uses the previously mentioned [`4.4 ops`](#44-ops) functions when responding to requests. + +### 5.1 Initialization +The service is started simply by calling: [`cuprate_database::service::init()`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/service/free.rs#L23). + +This function initializes the database, spawns threads, and returns a: +- Read handle to the database (cloneable) +- Write handle to the database (not cloneable) + +These "handles" implement the `tower::Service` trait, which allows sending requests and receiving responses `async`hronously. + +### 5.2 Requests +Along with the 2 handles, there are 2 types of requests: +- [`ReadRequest`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/types/src/service.rs#L23-L90) +- [`WriteRequest`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/types/src/service.rs#L93-L105) + +`ReadRequest` is for retrieving various types of information from the database. + +`WriteRequest` currently only has 1 variant: to write a block to the database. + +### 5.3 Responses +After sending one of the above requests using the read/write handle, the value returned is _not_ the response, yet an `async`hronous channel that will eventually return the response: +```rust,ignore +// Send a request. +// tower::Service::call() +// V +let response_channel: Channel = read_handle.call(ReadResponse::ChainHeight)?; + +// Await the response. +let response: ReadResponse = response_channel.await?; + +// Assert the response is what we expected. +assert_eq!(matches!(response), Response::ChainHeight(_)); +``` + +After `await`ing the returned channel, a `Response` will eventually be returned when the `service` threadpool has fetched the value from the database and sent it off. + +Both read/write requests variants match in name with `Response` variants, i.e. +- `ReadRequest::ChainHeight` leads to `Response::ChainHeight` +- `WriteRequest::WriteBlock` leads to `Response::WriteBlockOk` + +### 5.4 Thread model +As mentioned in the [`4. Layers`](#4-layers) section, the base database abstractions themselves are not concerned with parallelism, they are mostly functions to be called from a single-thread. + +However, the `cuprate_database::service` API, _does_ have a thread model backing it. + +When [`cuprate_database::service`'s initialization function](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/free.rs#L33-L44) is called, threads will be spawned and maintained until the user drops (disconnects) the returned handles. + +The current behavior for thread count is: +- [1 writer thread](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/write.rs#L52-L66) +- [As many reader threads as there are system threads](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/read.rs#L104-L126) + +For example, on a system with 32-threads, `cuprate_database` will spawn: +- 1 writer thread +- 32 reader threads + +whose sole responsibility is to listen for database requests, access the database (potentially in parallel), and return a response. + +Note that the `1 system thread = 1 reader thread` model is only the default setting, the reader thread count can be configured by the user to be any number between `1 .. amount_of_system_threads`. + +The reader threads are managed by [`rayon`](https://docs.rs/rayon). + +For an example of where multiple reader threads are used: given a request that asks if any key-image within a set already exists, `cuprate_database` will [split that work between the threads with `rayon`](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/read.rs#L490-L503). + +### 5.5 Shutdown +Once the read/write handles are `Drop`ed, the backing thread(pool) will gracefully exit, automatically. + +Note the writer thread and reader threadpool aren't connected whatsoever; dropping the write handle will make the writer thread exit, however, the reader handle is free to be held onto and can be continued to be read from - and vice-versa for the write handle. + +## 6. Syncing +`cuprate_database`'s database has 5 disk syncing modes. + +1. FastThenSafe +1. Safe +1. Async +1. Threshold +1. Fast + +The default mode is `Safe`. + +This means that upon each transaction commit, all the data that was written will be fully synced to disk. This is the slowest, but safest mode of operation. + +Note that upon any database `Drop`, whether via `service` or dropping the database directly, the current implementation will sync to disk regardless of any configuration. + +For more information on the other modes, read the documentation [here](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/config/sync_mode.rs#L63-L144). + +## 7. Resizing +Database backends that require manually resizing will, by default, use a similar algorithm as `monerod`'s. + +Note that this only relates to the `service` module, where the database is handled by `cuprate_database` itself, not the user. In the case of a user directly using `cuprate_database`, it is up to them on how to resize. + +Within `service`, the resizing logic defined [here](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/service/write.rs#L139-L201) does the following: + +- If there's not enough space to fit a write request's data, start a resize +- Each resize adds around [`1_073_745_920`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L104-L160) bytes to the current map size +- A resize will be attempted `3` times before failing + +There are other [resizing algorithms](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L38-L47) that define how the database's memory map grows, although currently the behavior of [`monerod`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L104-L160) is closely followed. + +## 8. (De)serialization +All types stored inside the database are either bytes already, or are perfectly bitcast-able. + +As such, they do not incur heavy (de)serialization costs when storing/fetching them from the database. The main (de)serialization used is [`bytemuck`](https://docs.rs/bytemuck)'s traits and casting functions. + +The size & layout of types is stable across compiler versions, as they are set and determined with [`#[repr(C)]`](https://doc.rust-lang.org/nomicon/other-reprs.html#reprc) and `bytemuck`'s derive macros such as [`bytemuck::Pod`](https://docs.rs/bytemuck/latest/bytemuck/derive.Pod.html). + +Note that the data stored in the tables are still type-safe; we still refer to the key and values within our tables by the type. + +The main deserialization `trait` for database storage is: [`cuprate_database::Storable`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L16-L115). + +- Before storage, the type is [simply cast into bytes](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L125) +- When fetching, the bytes are [simply cast into the type](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L130) + +When a type is casted into bytes, [the reference is casted](https://docs.rs/bytemuck/latest/bytemuck/fn.bytes_of.html), i.e. this is zero-cost serialization. + +However, it is worth noting that when bytes are casted into the type, [it is copied](https://docs.rs/bytemuck/latest/bytemuck/fn.pod_read_unaligned.html). This is due to byte alignment guarantee issues with both backends, see: +- https://github.com/AltSysrq/lmdb-zero/issues/8 +- https://github.com/cberner/redb/issues/360 + +Without this, `bytemuck` will panic with [`TargetAlignmentGreaterAndInputNotAligned`](https://docs.rs/bytemuck/latest/bytemuck/enum.PodCastError.html#variant.TargetAlignmentGreaterAndInputNotAligned) when casting. + +Copying the bytes fixes this problem, although it is more costly than necessary. However, in the main use-case for `cuprate_database` (the `service` module) the bytes would need to be owned regardless as the `Request/Response` API uses owned data types (`T`, `Vec<T>`, `HashMap<K, V>`, etc). + +Practically speaking, this means lower-level database functions that normally look like such: +```rust +fn get(key: &Key) -> &Value; +``` +end up looking like this in `cuprate_database`: +```rust +fn get(key: &Key) -> Value; +``` + +Since each backend has its own (de)serialization methods, our types are wrapped in compatibility types that map our `Storable` functions into whatever is required for the backend, e.g: +- [`StorableHeed<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/backend/heed/storable.rs#L11-L45) +- [`StorableRedb<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/backend/redb/storable.rs#L11-L30) + +Compatibility structs also exist for any `Storable` containers: +- [`StorableVec<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L135-L191) +- [`StorableBytes`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L208-L241) + +Again, it's unfortunate that these must be owned, although in `service`'s use-case, they would have to be owned anyway. + +## 9. Schema +This following section contains Cuprate's database schema, it may change throughout the development of Cuprate, as such, nothing here is final. + +### 9.1 Tables +The `CamelCase` names of the table headers documented here (e.g. `TxIds`) are the actual type name of the table within `cuprate_database`. + +Note that words written within `code blocks` mean that it is a real type defined and usable within `cuprate_database`. Other standard types like u64 and type aliases (TxId) are written normally. + +Within `cuprate_database::tables`, the below table is essentially defined as-is with [a macro](https://github.com/Cuprate/cuprate/blob/31ce89412aa174fc33754f22c9a6d9ef5ddeda28/database/src/tables.rs#L369-L470). + +Many of the data types stored are the same data types, although are different semantically, as such, a map of aliases used and their real data types is also provided below. + +| Alias | Real Type | +|----------------------------------------------------|-----------| +| BlockHeight, Amount, AmountIndex, TxId, UnlockTime | u64 +| BlockHash, KeyImage, TxHash, PrunableHash | [u8; 32] + +| Table | Key | Value | Description | +|-------------------|----------------------|--------------------|-------------| +| `BlockBlobs` | BlockHeight | `StorableVec<u8>` | Maps a block's height to a serialized byte form of a block +| `BlockHeights` | BlockHash | BlockHeight | Maps a block's hash to its height +| `BlockInfos` | BlockHeight | `BlockInfo` | Contains metadata of all blocks +| `KeyImages` | KeyImage | () | This table is a set with no value, it stores transaction key images +| `NumOutputs` | Amount | u64 | Maps an output's amount to the number of outputs with that amount +| `Outputs` | `PreRctOutputId` | `Output` | This table contains legacy CryptoNote outputs which have clear amounts. This table will not contain an output with 0 amount. +| `PrunedTxBlobs` | TxId | `StorableVec<u8>` | Contains pruned transaction blobs (even if the database is not pruned) +| `PrunableTxBlobs` | TxId | `StorableVec<u8>` | Contains the prunable part of a transaction +| `PrunableHashes` | TxId | PrunableHash | Contains the hash of the prunable part of a transaction +| `RctOutputs` | AmountIndex | `RctOutput` | Contains RingCT outputs mapped from their global RCT index +| `TxBlobs` | TxId | `StorableVec<u8>` | Serialized transaction blobs (bytes) +| `TxIds` | TxHash | TxId | Maps a transaction's hash to its index/ID +| `TxHeights` | TxId | BlockHeight | Maps a transaction's ID to the height of the block it comes from +| `TxOutputs` | TxId | `StorableVec<u64>` | Gives the amount indices of a transaction's outputs +| `TxUnlockTime` | TxId | UnlockTime | Stores the unlock time of a transaction (only if it has a non-zero lock time) + +The definitions for aliases and types (e.g. `RctOutput`) are within the [`cuprate_database::types`](https://github.com/Cuprate/cuprate/blob/31ce89412aa174fc33754f22c9a6d9ef5ddeda28/database/src/types.rs#L51) module. + +<!-- TODO(Boog900): We could split this table again into `RingCT (non-miner) Outputs` and `RingCT (miner) Outputs` as for miner outputs we can store the amount instead of commitment saving 24 bytes per miner output. --> + +### 9.2 Multimap tables +When referencing outputs, Monero will [use the amount and the amount index](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/blockchain_db/lmdb/db_lmdb.cpp#L3447-L3449). This means 2 keys are needed to reach an output. + +With LMDB you can set the `DUP_SORT` flag on a table and then set the key/value to: +```rust +Key = KEY_PART_1 +``` +```rust +Value = { + KEY_PART_2, + VALUE // The actual value we are storing. +} +``` + +Then you can set a custom value sorting function that only takes `KEY_PART_2` into account; this is how `monerod` does it. + +This requires that the underlying database supports: +- multimap tables +- custom sort functions on values +- setting a cursor on a specific key/value + +--- + +Another way to implement this is as follows: +```rust +Key = { KEY_PART_1, KEY_PART_2 } +``` +```rust +Value = VALUE +``` + +Then the key type is simply used to look up the value; this is how `cuprate_database` does it. + +For example, the key/value pair for outputs is: +```rust +PreRctOutputId => Output +``` +where `PreRctOutputId` looks like this: +```rust +struct PreRctOutputId { + amount: u64, + amount_index: u64, +} +``` + +## 10. Known issues and tradeoffs +`cuprate_database` takes many tradeoffs, whether due to: +- Prioritizing certain values over others +- Not having a better solution +- Being "good enough" + +This is a list of the larger ones, along with issues that don't have answers yet. + +### 10.1 Traits abstracting backends +Although all database backends used are very similar, they have some crucial differences in small implementation details that must be worked around when conforming them to `cuprate_database`'s traits. + +Put simply: using `cuprate_database`'s traits is less efficient and more awkward than using the backend directly. + +For example: +- [Data types must be wrapped in compatibility layers when they otherwise wouldn't be](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/backend/heed/env.rs#L101-L116) +- [There are types that only apply to a specific backend, but are visible to all](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/error.rs#L86-L89) +- [There are extra layers of abstraction to smoothen the differences between all backends](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/env.rs#L62-L68) +- [Existing functionality of backends must be taken away, as it isn't supported in the others](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/database.rs#L27-L34) + +This is a _tradeoff_ that `cuprate_database` takes, as: +- The backend itself is usually not the source of bottlenecks in the greater system, as such, small inefficiencies are OK +- None of the lost functionality is crucial for operation +- The ability to use, test, and swap between multiple database backends is [worth it](https://github.com/Cuprate/cuprate/pull/35#issuecomment-1952804393) + +### 10.2 Hot-swappable backends +Using a different backend is really as simple as re-building `cuprate_database` with a different feature flag: +```bash +# Use LMDB. +cargo build --package cuprate-database --features heed + +# Use redb. +cargo build --package cuprate-database --features redb +``` + +This is "good enough" for now, however ideally, this hot-swapping of backends would be able to be done at _runtime_. + +As it is now, `cuprate_database` cannot compile both backends and swap based on user input at runtime; it must be compiled with a certain backend, which will produce a binary with only that backend. + +This also means things like [CI testing multiple backends is awkward](https://github.com/Cuprate/cuprate/blob/main/.github/workflows/ci.yml#L132-L136), as we must re-compile with different feature flags instead. + +### 10.3 Copying unaligned bytes +As mentioned in [`8. (De)serialization`](#8-deserialization), bytes are _copied_ when they are turned into a type `T` due to unaligned bytes being returned from database backends. + +Using a regular reference cast results in an improperly aligned type `T`; [such a type even existing causes undefined behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html). In our case, `bytemuck` saves us by panicking before this occurs. + +Thus, when using `cuprate_database`'s database traits, an _owned_ `T` is returned. + +This is doubly unfortunately for `&[u8]` as this does not even need deserialization. + +For example, `StorableVec` could have been this: +```rust +enum StorableBytes<'a, T: Storable> { + Owned(T), + Ref(&'a T), +} +``` +but this would require supporting types that must be copied regardless with the occasional `&[u8]` that can be returned without casting. This was hard to do so in a generic way, thus all `[u8]`'s are copied and returned as owned `StorableVec`s. + +This is a _tradeoff_ `cuprate_database` takes as: +- `bytemuck::pod_read_unaligned` is cheap enough +- The main API, `service`, needs to return owned value anyway +- Having no references removes a lot of lifetime complexity + +The alternative is either: +- Using proper (de)serialization instead of casting (which comes with its own costs) +- Somehow fixing the alignment issues in the backends mentioned previously + +### 10.4 Endianness +`cuprate_database`'s (de)serialization and storage of bytes are native-endian, as in, byte storage order will depend on the machine it is running on. + +As Cuprate's build-targets are all little-endian ([big-endian by default machines barely exist](https://en.wikipedia.org/wiki/Endianness#Hardware)), this doesn't matter much and the byte ordering can be seen as a constant. + +Practically, this means `cuprated`'s database files can be transferred across computers, as can `monerod`'s. + +### 10.5 Extra table data +Some of `cuprate_database`'s tables differ from `monerod`'s tables, for example, the way [`9.2 Multimap tables`](#92-multimap-tables) tables are done requires that the primary key is stored _for all_ entries, compared to `monerod` only needing to store it once. + +For example: +```rust +// `monerod` only stores `amount: 1` once, +// `cuprated` stores it each time it appears. +struct PreRctOutputId { amount: 1, amount_index: 0 } +struct PreRctOutputId { amount: 1, amount_index: 1 } +``` + +This means `cuprated`'s database will be slightly larger than `monerod`'s. + +The current method `cuprate_database` uses will be "good enough" until usage shows that it must be optimized as multimap tables are tricky to implement across all backends. \ No newline at end of file diff --git a/database/src/backend/heed/database.rs b/storage/cuprate-database/src/backend/heed/database.rs similarity index 100% rename from database/src/backend/heed/database.rs rename to storage/cuprate-database/src/backend/heed/database.rs diff --git a/database/src/backend/heed/env.rs b/storage/cuprate-database/src/backend/heed/env.rs similarity index 100% rename from database/src/backend/heed/env.rs rename to storage/cuprate-database/src/backend/heed/env.rs diff --git a/database/src/backend/heed/error.rs b/storage/cuprate-database/src/backend/heed/error.rs similarity index 100% rename from database/src/backend/heed/error.rs rename to storage/cuprate-database/src/backend/heed/error.rs diff --git a/database/src/backend/heed/mod.rs b/storage/cuprate-database/src/backend/heed/mod.rs similarity index 100% rename from database/src/backend/heed/mod.rs rename to storage/cuprate-database/src/backend/heed/mod.rs diff --git a/database/src/backend/heed/storable.rs b/storage/cuprate-database/src/backend/heed/storable.rs similarity index 100% rename from database/src/backend/heed/storable.rs rename to storage/cuprate-database/src/backend/heed/storable.rs diff --git a/database/src/backend/heed/transaction.rs b/storage/cuprate-database/src/backend/heed/transaction.rs similarity index 100% rename from database/src/backend/heed/transaction.rs rename to storage/cuprate-database/src/backend/heed/transaction.rs diff --git a/database/src/backend/heed/types.rs b/storage/cuprate-database/src/backend/heed/types.rs similarity index 100% rename from database/src/backend/heed/types.rs rename to storage/cuprate-database/src/backend/heed/types.rs diff --git a/database/src/backend/mod.rs b/storage/cuprate-database/src/backend/mod.rs similarity index 100% rename from database/src/backend/mod.rs rename to storage/cuprate-database/src/backend/mod.rs diff --git a/database/src/backend/redb/database.rs b/storage/cuprate-database/src/backend/redb/database.rs similarity index 100% rename from database/src/backend/redb/database.rs rename to storage/cuprate-database/src/backend/redb/database.rs diff --git a/database/src/backend/redb/env.rs b/storage/cuprate-database/src/backend/redb/env.rs similarity index 100% rename from database/src/backend/redb/env.rs rename to storage/cuprate-database/src/backend/redb/env.rs diff --git a/database/src/backend/redb/error.rs b/storage/cuprate-database/src/backend/redb/error.rs similarity index 100% rename from database/src/backend/redb/error.rs rename to storage/cuprate-database/src/backend/redb/error.rs diff --git a/database/src/backend/redb/mod.rs b/storage/cuprate-database/src/backend/redb/mod.rs similarity index 100% rename from database/src/backend/redb/mod.rs rename to storage/cuprate-database/src/backend/redb/mod.rs diff --git a/database/src/backend/redb/storable.rs b/storage/cuprate-database/src/backend/redb/storable.rs similarity index 100% rename from database/src/backend/redb/storable.rs rename to storage/cuprate-database/src/backend/redb/storable.rs diff --git a/database/src/backend/redb/transaction.rs b/storage/cuprate-database/src/backend/redb/transaction.rs similarity index 100% rename from database/src/backend/redb/transaction.rs rename to storage/cuprate-database/src/backend/redb/transaction.rs diff --git a/database/src/backend/redb/types.rs b/storage/cuprate-database/src/backend/redb/types.rs similarity index 100% rename from database/src/backend/redb/types.rs rename to storage/cuprate-database/src/backend/redb/types.rs diff --git a/database/src/backend/tests.rs b/storage/cuprate-database/src/backend/tests.rs similarity index 100% rename from database/src/backend/tests.rs rename to storage/cuprate-database/src/backend/tests.rs diff --git a/database/src/config/backend.rs b/storage/cuprate-database/src/config/backend.rs similarity index 100% rename from database/src/config/backend.rs rename to storage/cuprate-database/src/config/backend.rs diff --git a/database/src/config/config.rs b/storage/cuprate-database/src/config/config.rs similarity index 100% rename from database/src/config/config.rs rename to storage/cuprate-database/src/config/config.rs diff --git a/database/src/config/mod.rs b/storage/cuprate-database/src/config/mod.rs similarity index 100% rename from database/src/config/mod.rs rename to storage/cuprate-database/src/config/mod.rs diff --git a/database/src/config/reader_threads.rs b/storage/cuprate-database/src/config/reader_threads.rs similarity index 100% rename from database/src/config/reader_threads.rs rename to storage/cuprate-database/src/config/reader_threads.rs diff --git a/database/src/config/sync_mode.rs b/storage/cuprate-database/src/config/sync_mode.rs similarity index 100% rename from database/src/config/sync_mode.rs rename to storage/cuprate-database/src/config/sync_mode.rs diff --git a/database/src/constants.rs b/storage/cuprate-database/src/constants.rs similarity index 100% rename from database/src/constants.rs rename to storage/cuprate-database/src/constants.rs diff --git a/database/src/database.rs b/storage/cuprate-database/src/database.rs similarity index 100% rename from database/src/database.rs rename to storage/cuprate-database/src/database.rs diff --git a/database/src/env.rs b/storage/cuprate-database/src/env.rs similarity index 100% rename from database/src/env.rs rename to storage/cuprate-database/src/env.rs diff --git a/database/src/error.rs b/storage/cuprate-database/src/error.rs similarity index 100% rename from database/src/error.rs rename to storage/cuprate-database/src/error.rs diff --git a/database/src/free.rs b/storage/cuprate-database/src/free.rs similarity index 100% rename from database/src/free.rs rename to storage/cuprate-database/src/free.rs diff --git a/database/src/key.rs b/storage/cuprate-database/src/key.rs similarity index 100% rename from database/src/key.rs rename to storage/cuprate-database/src/key.rs diff --git a/database/src/lib.rs b/storage/cuprate-database/src/lib.rs similarity index 100% rename from database/src/lib.rs rename to storage/cuprate-database/src/lib.rs diff --git a/database/src/ops/block.rs b/storage/cuprate-database/src/ops/block.rs similarity index 100% rename from database/src/ops/block.rs rename to storage/cuprate-database/src/ops/block.rs diff --git a/database/src/ops/blockchain.rs b/storage/cuprate-database/src/ops/blockchain.rs similarity index 100% rename from database/src/ops/blockchain.rs rename to storage/cuprate-database/src/ops/blockchain.rs diff --git a/database/src/ops/key_image.rs b/storage/cuprate-database/src/ops/key_image.rs similarity index 100% rename from database/src/ops/key_image.rs rename to storage/cuprate-database/src/ops/key_image.rs diff --git a/database/src/ops/macros.rs b/storage/cuprate-database/src/ops/macros.rs similarity index 100% rename from database/src/ops/macros.rs rename to storage/cuprate-database/src/ops/macros.rs diff --git a/database/src/ops/mod.rs b/storage/cuprate-database/src/ops/mod.rs similarity index 100% rename from database/src/ops/mod.rs rename to storage/cuprate-database/src/ops/mod.rs diff --git a/database/src/ops/output.rs b/storage/cuprate-database/src/ops/output.rs similarity index 100% rename from database/src/ops/output.rs rename to storage/cuprate-database/src/ops/output.rs diff --git a/database/src/ops/property.rs b/storage/cuprate-database/src/ops/property.rs similarity index 100% rename from database/src/ops/property.rs rename to storage/cuprate-database/src/ops/property.rs diff --git a/database/src/ops/tx.rs b/storage/cuprate-database/src/ops/tx.rs similarity index 100% rename from database/src/ops/tx.rs rename to storage/cuprate-database/src/ops/tx.rs diff --git a/database/src/resize.rs b/storage/cuprate-database/src/resize.rs similarity index 100% rename from database/src/resize.rs rename to storage/cuprate-database/src/resize.rs diff --git a/database/src/service/free.rs b/storage/cuprate-database/src/service/free.rs similarity index 100% rename from database/src/service/free.rs rename to storage/cuprate-database/src/service/free.rs diff --git a/database/src/service/mod.rs b/storage/cuprate-database/src/service/mod.rs similarity index 100% rename from database/src/service/mod.rs rename to storage/cuprate-database/src/service/mod.rs diff --git a/database/src/service/read.rs b/storage/cuprate-database/src/service/read.rs similarity index 100% rename from database/src/service/read.rs rename to storage/cuprate-database/src/service/read.rs diff --git a/database/src/service/tests.rs b/storage/cuprate-database/src/service/tests.rs similarity index 100% rename from database/src/service/tests.rs rename to storage/cuprate-database/src/service/tests.rs diff --git a/database/src/service/types.rs b/storage/cuprate-database/src/service/types.rs similarity index 100% rename from database/src/service/types.rs rename to storage/cuprate-database/src/service/types.rs diff --git a/database/src/service/write.rs b/storage/cuprate-database/src/service/write.rs similarity index 100% rename from database/src/service/write.rs rename to storage/cuprate-database/src/service/write.rs diff --git a/database/src/storable.rs b/storage/cuprate-database/src/storable.rs similarity index 100% rename from database/src/storable.rs rename to storage/cuprate-database/src/storable.rs diff --git a/database/src/table.rs b/storage/cuprate-database/src/table.rs similarity index 100% rename from database/src/table.rs rename to storage/cuprate-database/src/table.rs diff --git a/database/src/tables.rs b/storage/cuprate-database/src/tables.rs similarity index 100% rename from database/src/tables.rs rename to storage/cuprate-database/src/tables.rs diff --git a/database/src/tests.rs b/storage/cuprate-database/src/tests.rs similarity index 100% rename from database/src/tests.rs rename to storage/cuprate-database/src/tests.rs diff --git a/database/src/transaction.rs b/storage/cuprate-database/src/transaction.rs similarity index 100% rename from database/src/transaction.rs rename to storage/cuprate-database/src/transaction.rs diff --git a/database/src/types.rs b/storage/cuprate-database/src/types.rs similarity index 100% rename from database/src/types.rs rename to storage/cuprate-database/src/types.rs diff --git a/database/src/unsafe_sendable.rs b/storage/cuprate-database/src/unsafe_sendable.rs similarity index 100% rename from database/src/unsafe_sendable.rs rename to storage/cuprate-database/src/unsafe_sendable.rs diff --git a/storage/database/Cargo.toml b/storage/database/Cargo.toml new file mode 100644 index 00000000..712dbb16 --- /dev/null +++ b/storage/database/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "cuprate-database" +version = "0.0.0" +edition = "2021" +description = "Cuprate's database abstraction" +license = "MIT" +authors = ["hinto-janai"] +repository = "https://github.com/Cuprate/cuprate/tree/main/database" +keywords = ["cuprate", "database"] + +[features] +default = ["heed", "redb", "service"] +# default = ["redb", "service"] +# default = ["redb-memory", "service"] +heed = ["dep:heed"] +redb = ["dep:redb"] +redb-memory = ["redb"] +service = ["dep:crossbeam", "dep:futures", "dep:tokio", "dep:tokio-util", "dep:tower", "dep:rayon"] + +[dependencies] +bitflags = { workspace = true, features = ["serde", "bytemuck"] } +bytemuck = { version = "1.14.3", features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] } +bytes = { workspace = true } +cfg-if = { workspace = true } +# FIXME: +# We only need the `thread` feature if `service` is enabled. +# Figure out how to enable features of an already pulled in dependency conditionally. +cuprate-helper = { path = "../helper", features = ["fs", "thread", "map"] } +cuprate-types = { path = "../types", features = ["service"] } +curve25519-dalek = { workspace = true } +monero-pruning = { path = "../pruning" } +monero-serai = { workspace = true, features = ["std"] } +paste = { workspace = true } +page_size = { version = "0.6.0" } # Needed for database resizes, they must be a multiple of the OS page size. +thiserror = { workspace = true } + +# `service` feature. +crossbeam = { workspace = true, features = ["std"], optional = true } +futures = { workspace = true, optional = true } +tokio = { workspace = true, features = ["full"], optional = true } +tokio-util = { workspace = true, features = ["full"], optional = true } +tower = { workspace = true, features = ["full"], optional = true } +thread_local = { workspace = true } +rayon = { workspace = true, optional = true } + +# Optional features. +heed = { version = "0.20.0", features = ["read-txn-no-tls"], optional = true } +redb = { version = "2.1.0", optional = true } +serde = { workspace = true, optional = true } + +[dev-dependencies] +bytemuck = { version = "1.14.3", features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] } +cuprate-helper = { path = "../helper", features = ["thread"] } +cuprate-test-utils = { path = "../test-utils" } +page_size = { version = "0.6.0" } +tempfile = { version = "3.10.0" } +pretty_assertions = { workspace = true } +hex = { workspace = true } +hex-literal = { workspace = true } \ No newline at end of file diff --git a/storage/database/README.md b/storage/database/README.md new file mode 100644 index 00000000..293413ac --- /dev/null +++ b/storage/database/README.md @@ -0,0 +1,598 @@ +# Database +Cuprate's database implementation. + +- [1. Documentation](#1-documentation) +- [2. File structure](#2-file-structure) + - [2.1 `src/`](#21-src) + - [2.2 `src/backend/`](#22-srcbackend) + - [2.3 `src/config/`](#23-srcconfig) + - [2.4 `src/ops/`](#24-srcops) + - [2.5 `src/service/`](#25-srcservice) +- [3. Backends](#3-backends) + - [3.1 heed](#31-heed) + - [3.2 redb](#32-redb) + - [3.3 redb-memory](#33-redb-memory) + - [3.4 sanakirja](#34-sanakirja) + - [3.5 MDBX](#35-mdbx) +- [4. Layers](#4-layers) + - [4.1 Backend](#41-backend) + - [4.2 Trait](#42-trait) + - [4.3 ConcreteEnv](#43-concreteenv) + - [4.4 ops](#44-ops) + - [4.5 service](#45-service) +- [5. The service](#5-the-service) + - [5.1 Initialization](#51-initialization) + - [5.2 Requests](#53-requests) + - [5.3 Responses](#54-responses) + - [5.4 Thread model](#52-thread-model) + - [5.5 Shutdown](#55-shutdown) +- [6. Syncing](#6-Syncing) +- [7. Resizing](#7-resizing) +- [8. (De)serialization](#8-deserialization) +- [9. Schema](#9-schema) + - [9.1 Tables](#91-tables) + - [9.2 Multimap tables](#92-multimap-tables) +- [10. Known issues and tradeoffs](#10-known-issues-and-tradeoffs) + - [10.1 Traits abstracting backends](#101-traits-abstracting-backends) + - [10.2 Hot-swappable backends](#102-hot-swappable-backends) + - [10.3 Copying unaligned bytes](#103-copying-unaligned-bytes) + - [10.4 Endianness](#104-endianness) + - [10.5 Extra table data](#105-extra-table-data) + +--- + +## 1. Documentation +Documentation for `database/` is split into 3 locations: + +| Documentation location | Purpose | +|---------------------------|---------| +| `database/README.md` | High level design of `cuprate-database` +| `cuprate-database` | Practical usage documentation/warnings/notes/etc +| Source file `// comments` | Implementation-specific details (e.g, how many reader threads to spawn?) + +This README serves as the implementation design document. + +For actual practical usage, `cuprate-database`'s types and general usage are documented via standard Rust tooling. + +Run: +```bash +cargo doc --package cuprate-database --open +``` +at the root of the repo to open/read the documentation. + +If this documentation is too abstract, refer to any of the source files, they are heavily commented. There are many `// Regular comments` that explain more implementation specific details that aren't present here or in the docs. Use the file reference below to find what you're looking for. + +The code within `src/` is also littered with some `grep`-able comments containing some keywords: + +| Word | Meaning | +|-------------|---------| +| `INVARIANT` | This code makes an _assumption_ that must be upheld for correctness +| `SAFETY` | This `unsafe` code is okay, for `x,y,z` reasons +| `FIXME` | This code works but isn't ideal +| `HACK` | This code is a brittle workaround +| `PERF` | This code is weird for performance reasons +| `TODO` | This must be implemented; There should be 0 of these in production code +| `SOMEDAY` | This should be implemented... someday + +## 2. File structure +A quick reference of the structure of the folders & files in `cuprate-database`. + +Note that `lib.rs/mod.rs` files are purely for re-exporting/visibility/lints, and contain no code. Each sub-directory has a corresponding `mod.rs`. + +### 2.1 `src/` +The top-level `src/` files. + +| File | Purpose | +|------------------------|---------| +| `constants.rs` | General constants used throughout `cuprate-database` +| `database.rs` | Abstracted database; `trait DatabaseR{o,w}` +| `env.rs` | Abstracted database environment; `trait Env` +| `error.rs` | Database error types +| `free.rs` | General free functions (related to the database) +| `key.rs` | Abstracted database keys; `trait Key` +| `resize.rs` | Database resizing algorithms +| `storable.rs` | Data (de)serialization; `trait Storable` +| `table.rs` | Database table abstraction; `trait Table` +| `tables.rs` | All the table definitions used by `cuprate-database` +| `tests.rs` | Utilities for `cuprate_database` testing +| `transaction.rs` | Database transaction abstraction; `trait TxR{o,w}` +| `types.rs` | Database-specific types +| `unsafe_unsendable.rs` | Marker type to impl `Send` for objects not `Send` + +### 2.2 `src/backend/` +This folder contains the implementation for actual databases used as the backend for `cuprate-database`. + +Each backend has its own folder. + +| Folder/File | Purpose | +|-------------|---------| +| `heed/` | Backend using using [`heed`](https://github.com/meilisearch/heed) (LMDB) +| `redb/` | Backend using [`redb`](https://github.com/cberner/redb) +| `tests.rs` | Backend-agnostic tests + +All backends follow the same file structure: + +| File | Purpose | +|------------------|---------| +| `database.rs` | Implementation of `trait DatabaseR{o,w}` +| `env.rs` | Implementation of `trait Env` +| `error.rs` | Implementation of backend's errors to `cuprate_database`'s error types +| `storable.rs` | Compatibility layer between `cuprate_database::Storable` and backend-specific (de)serialization +| `transaction.rs` | Implementation of `trait TxR{o,w}` +| `types.rs` | Type aliases for long backend-specific types + +### 2.3 `src/config/` +This folder contains the `cupate_database::config` module; configuration options for the database. + +| File | Purpose | +|---------------------|---------| +| `config.rs` | Main database `Config` struct +| `reader_threads.rs` | Reader thread configuration for `service` thread-pool +| `sync_mode.rs` | Disk sync configuration for backends + +### 2.4 `src/ops/` +This folder contains the `cupate_database::ops` module. + +These are higher-level functions abstracted over the database, that are Monero-related. + +| File | Purpose | +|-----------------|---------| +| `block.rs` | Block related (main functions) +| `blockchain.rs` | Blockchain related (height, cumulative values, etc) +| `key_image.rs` | Key image related +| `macros.rs` | Macros specific to `ops/` +| `output.rs` | Output related +| `property.rs` | Database properties (pruned, version, etc) +| `tx.rs` | Transaction related + +### 2.5 `src/service/` +This folder contains the `cupate_database::service` module. + +The `async`hronous request/response API other Cuprate crates use instead of managing the database directly themselves. + +| File | Purpose | +|----------------|---------| +| `free.rs` | General free functions used (related to `cuprate_database::service`) +| `read.rs` | Read thread-pool definitions and logic +| `tests.rs` | Thread-pool tests and test helper functions +| `types.rs` | `cuprate_database::service`-related type aliases +| `write.rs` | Writer thread definitions and logic + +## 3. Backends +`cuprate-database`'s `trait`s allow abstracting over the actual database, such that any backend in particular could be used. + +Each database's implementation for those `trait`'s are located in its respective folder in `src/backend/${DATABASE_NAME}/`. + +### 3.1 heed +The default database used is [`heed`](https://github.com/meilisearch/heed) (LMDB). The upstream versions from [`crates.io`](https://crates.io/crates/heed) are used. `LMDB` should not need to be installed as `heed` has a build script that pulls it in automatically. + +`heed`'s filenames inside Cuprate's database folder (`~/.local/share/cuprate/database/`) are: + +| Filename | Purpose | +|------------|---------| +| `data.mdb` | Main data file +| `lock.mdb` | Database lock file + +`heed`-specific notes: +- [There is a maximum reader limit](https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1372). Other potential processes (e.g. `xmrblocks`) that are also reading the `data.mdb` file need to be accounted for +- [LMDB does not work on remote filesystem](https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/lmdb.h#L129) + +### 3.2 redb +The 2nd database backend is the 100% Rust [`redb`](https://github.com/cberner/redb). + +The upstream versions from [`crates.io`](https://crates.io/crates/redb) are used. + +`redb`'s filenames inside Cuprate's database folder (`~/.local/share/cuprate/database/`) are: + +| Filename | Purpose | +|-------------|---------| +| `data.redb` | Main data file + +<!-- TODO: document DB on remote filesystem (does redb allow this?) --> + +### 3.3 redb-memory +This backend is 100% the same as `redb`, although, it uses `redb::backend::InMemoryBackend` which is a database that completely resides in memory instead of a file. + +All other details about this should be the same as the normal `redb` backend. + +### 3.4 sanakirja +[`sanakirja`](https://docs.rs/sanakirja) was a candidate as a backend, however there were problems with maximum value sizes. + +The default maximum value size is [1012 bytes](https://docs.rs/sanakirja/1.4.1/sanakirja/trait.Storable.html) which was too small for our requirements. Using [`sanakirja::Slice`](https://docs.rs/sanakirja/1.4.1/sanakirja/union.Slice.html) and [sanakirja::UnsizedStorage](https://docs.rs/sanakirja/1.4.1/sanakirja/trait.UnsizedStorable.html) was attempted, but there were bugs found when inserting a value in-between `512..=4096` bytes. + +As such, it is not implemented. + +### 3.5 MDBX +[`MDBX`](https://erthink.github.io/libmdbx) was a candidate as a backend, however MDBX deprecated the custom key/value comparison functions, this makes it a bit trickier to implement [`9.2 Multimap tables`](#92-multimap-tables). It is also quite similar to the main backend LMDB (of which it was originally a fork of). + +As such, it is not implemented (yet). + +## 4. Layers +`cuprate_database` is logically abstracted into 5 layers, with each layer being built upon the last. + +Starting from the lowest: +1. Backend +2. Trait +3. ConcreteEnv +4. `ops` +5. `service` + +<!-- TODO: insert image here after database/ split --> + +### 4.1 Backend +This is the actual database backend implementation (or a Rust shim over one). + +Examples: +- `heed` (LMDB) +- `redb` + +`cuprate_database` itself just uses a backend, it does not implement one. + +All backends have the following attributes: +- [Embedded](https://en.wikipedia.org/wiki/Embedded_database) +- [Multiversion concurrency control](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) +- [ACID](https://en.wikipedia.org/wiki/ACID) +- Are `(key, value)` oriented and have the expected API (`get()`, `insert()`, `delete()`) +- Are table oriented (`"table_name" -> (key, value)`) +- Allows concurrent readers + +### 4.2 Trait +`cuprate_database` provides a set of `trait`s that abstract over the various database backends. + +This allows the function signatures and behavior to stay the same but allows for swapping out databases in an easier fashion. + +All common behavior of the backend's are encapsulated here and used instead of using the backend directly. + +Examples: +- [`trait Env`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/env.rs) +- [`trait {TxRo, TxRw}`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/transaction.rs) +- [`trait {DatabaseRo, DatabaseRw}`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/database.rs) + +For example, instead of calling `LMDB` or `redb`'s `get()` function directly, `DatabaseRo::get()` is called. + +### 4.3 ConcreteEnv +This is the non-generic, concrete `struct` provided by `cuprate_database` that contains all the data necessary to operate the database. The actual database backend `ConcreteEnv` will use internally depends on which backend feature is used. + +`ConcreteEnv` implements `trait Env`, which opens the door to all the other traits. + +The equivalent objects in the backends themselves are: +- [`heed::Env`](https://docs.rs/heed/0.20.0/heed/struct.Env.html) +- [`redb::Database`](https://docs.rs/redb/2.1.0/redb/struct.Database.html) + +This is the main object used when handling the database directly, although that is not strictly necessary as a user if the [`4.5 service`](#45-service) layer is used. + +### 4.4 ops +These are Monero-specific functions that use the abstracted `trait` forms of the database. + +Instead of dealing with the database directly: +- `get()` +- `delete()` + +the `ops` layer provides more abstract functions that deal with commonly used Monero operations: +- `add_block()` +- `pop_block()` + +### 4.5 service +The final layer abstracts the database completely into a [Monero-specific `async` request/response API](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/types/src/service.rs#L18-L78) using [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html). + +For more information on this layer, see the next section: [`5. The service`](#5-the-service). + +## 5. The service +The main API `cuprate_database` exposes for other crates to use is the `cuprate_database::service` module. + +This module exposes an `async` request/response API with `tower::Service`, backed by a threadpool, that allows reading/writing Monero-related data from/to the database. + +`cuprate_database::service` itself manages the database using a separate writer thread & reader thread-pool, and uses the previously mentioned [`4.4 ops`](#44-ops) functions when responding to requests. + +### 5.1 Initialization +The service is started simply by calling: [`cuprate_database::service::init()`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/service/free.rs#L23). + +This function initializes the database, spawns threads, and returns a: +- Read handle to the database (cloneable) +- Write handle to the database (not cloneable) + +These "handles" implement the `tower::Service` trait, which allows sending requests and receiving responses `async`hronously. + +### 5.2 Requests +Along with the 2 handles, there are 2 types of requests: +- [`ReadRequest`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/types/src/service.rs#L23-L90) +- [`WriteRequest`](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/types/src/service.rs#L93-L105) + +`ReadRequest` is for retrieving various types of information from the database. + +`WriteRequest` currently only has 1 variant: to write a block to the database. + +### 5.3 Responses +After sending one of the above requests using the read/write handle, the value returned is _not_ the response, yet an `async`hronous channel that will eventually return the response: +```rust,ignore +// Send a request. +// tower::Service::call() +// V +let response_channel: Channel = read_handle.call(ReadResponse::ChainHeight)?; + +// Await the response. +let response: ReadResponse = response_channel.await?; + +// Assert the response is what we expected. +assert_eq!(matches!(response), Response::ChainHeight(_)); +``` + +After `await`ing the returned channel, a `Response` will eventually be returned when the `service` threadpool has fetched the value from the database and sent it off. + +Both read/write requests variants match in name with `Response` variants, i.e. +- `ReadRequest::ChainHeight` leads to `Response::ChainHeight` +- `WriteRequest::WriteBlock` leads to `Response::WriteBlockOk` + +### 5.4 Thread model +As mentioned in the [`4. Layers`](#4-layers) section, the base database abstractions themselves are not concerned with parallelism, they are mostly functions to be called from a single-thread. + +However, the `cuprate_database::service` API, _does_ have a thread model backing it. + +When [`cuprate_database::service`'s initialization function](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/free.rs#L33-L44) is called, threads will be spawned and maintained until the user drops (disconnects) the returned handles. + +The current behavior for thread count is: +- [1 writer thread](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/write.rs#L52-L66) +- [As many reader threads as there are system threads](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/read.rs#L104-L126) + +For example, on a system with 32-threads, `cuprate_database` will spawn: +- 1 writer thread +- 32 reader threads + +whose sole responsibility is to listen for database requests, access the database (potentially in parallel), and return a response. + +Note that the `1 system thread = 1 reader thread` model is only the default setting, the reader thread count can be configured by the user to be any number between `1 .. amount_of_system_threads`. + +The reader threads are managed by [`rayon`](https://docs.rs/rayon). + +For an example of where multiple reader threads are used: given a request that asks if any key-image within a set already exists, `cuprate_database` will [split that work between the threads with `rayon`](https://github.com/Cuprate/cuprate/blob/9c27ba5791377d639cb5d30d0f692c228568c122/database/src/service/read.rs#L490-L503). + +### 5.5 Shutdown +Once the read/write handles are `Drop`ed, the backing thread(pool) will gracefully exit, automatically. + +Note the writer thread and reader threadpool aren't connected whatsoever; dropping the write handle will make the writer thread exit, however, the reader handle is free to be held onto and can be continued to be read from - and vice-versa for the write handle. + +## 6. Syncing +`cuprate_database`'s database has 5 disk syncing modes. + +1. FastThenSafe +1. Safe +1. Async +1. Threshold +1. Fast + +The default mode is `Safe`. + +This means that upon each transaction commit, all the data that was written will be fully synced to disk. This is the slowest, but safest mode of operation. + +Note that upon any database `Drop`, whether via `service` or dropping the database directly, the current implementation will sync to disk regardless of any configuration. + +For more information on the other modes, read the documentation [here](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/config/sync_mode.rs#L63-L144). + +## 7. Resizing +Database backends that require manually resizing will, by default, use a similar algorithm as `monerod`'s. + +Note that this only relates to the `service` module, where the database is handled by `cuprate_database` itself, not the user. In the case of a user directly using `cuprate_database`, it is up to them on how to resize. + +Within `service`, the resizing logic defined [here](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/service/write.rs#L139-L201) does the following: + +- If there's not enough space to fit a write request's data, start a resize +- Each resize adds around [`1_073_745_920`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L104-L160) bytes to the current map size +- A resize will be attempted `3` times before failing + +There are other [resizing algorithms](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L38-L47) that define how the database's memory map grows, although currently the behavior of [`monerod`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/resize.rs#L104-L160) is closely followed. + +## 8. (De)serialization +All types stored inside the database are either bytes already, or are perfectly bitcast-able. + +As such, they do not incur heavy (de)serialization costs when storing/fetching them from the database. The main (de)serialization used is [`bytemuck`](https://docs.rs/bytemuck)'s traits and casting functions. + +The size & layout of types is stable across compiler versions, as they are set and determined with [`#[repr(C)]`](https://doc.rust-lang.org/nomicon/other-reprs.html#reprc) and `bytemuck`'s derive macros such as [`bytemuck::Pod`](https://docs.rs/bytemuck/latest/bytemuck/derive.Pod.html). + +Note that the data stored in the tables are still type-safe; we still refer to the key and values within our tables by the type. + +The main deserialization `trait` for database storage is: [`cuprate_database::Storable`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L16-L115). + +- Before storage, the type is [simply cast into bytes](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L125) +- When fetching, the bytes are [simply cast into the type](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L130) + +When a type is casted into bytes, [the reference is casted](https://docs.rs/bytemuck/latest/bytemuck/fn.bytes_of.html), i.e. this is zero-cost serialization. + +However, it is worth noting that when bytes are casted into the type, [it is copied](https://docs.rs/bytemuck/latest/bytemuck/fn.pod_read_unaligned.html). This is due to byte alignment guarantee issues with both backends, see: +- https://github.com/AltSysrq/lmdb-zero/issues/8 +- https://github.com/cberner/redb/issues/360 + +Without this, `bytemuck` will panic with [`TargetAlignmentGreaterAndInputNotAligned`](https://docs.rs/bytemuck/latest/bytemuck/enum.PodCastError.html#variant.TargetAlignmentGreaterAndInputNotAligned) when casting. + +Copying the bytes fixes this problem, although it is more costly than necessary. However, in the main use-case for `cuprate_database` (the `service` module) the bytes would need to be owned regardless as the `Request/Response` API uses owned data types (`T`, `Vec<T>`, `HashMap<K, V>`, etc). + +Practically speaking, this means lower-level database functions that normally look like such: +```rust +fn get(key: &Key) -> &Value; +``` +end up looking like this in `cuprate_database`: +```rust +fn get(key: &Key) -> Value; +``` + +Since each backend has its own (de)serialization methods, our types are wrapped in compatibility types that map our `Storable` functions into whatever is required for the backend, e.g: +- [`StorableHeed<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/backend/heed/storable.rs#L11-L45) +- [`StorableRedb<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/backend/redb/storable.rs#L11-L30) + +Compatibility structs also exist for any `Storable` containers: +- [`StorableVec<T>`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L135-L191) +- [`StorableBytes`](https://github.com/Cuprate/cuprate/blob/2ac90420c658663564a71b7ecb52d74f3c2c9d0f/database/src/storable.rs#L208-L241) + +Again, it's unfortunate that these must be owned, although in `service`'s use-case, they would have to be owned anyway. + +## 9. Schema +This following section contains Cuprate's database schema, it may change throughout the development of Cuprate, as such, nothing here is final. + +### 9.1 Tables +The `CamelCase` names of the table headers documented here (e.g. `TxIds`) are the actual type name of the table within `cuprate_database`. + +Note that words written within `code blocks` mean that it is a real type defined and usable within `cuprate_database`. Other standard types like u64 and type aliases (TxId) are written normally. + +Within `cuprate_database::tables`, the below table is essentially defined as-is with [a macro](https://github.com/Cuprate/cuprate/blob/31ce89412aa174fc33754f22c9a6d9ef5ddeda28/database/src/tables.rs#L369-L470). + +Many of the data types stored are the same data types, although are different semantically, as such, a map of aliases used and their real data types is also provided below. + +| Alias | Real Type | +|----------------------------------------------------|-----------| +| BlockHeight, Amount, AmountIndex, TxId, UnlockTime | u64 +| BlockHash, KeyImage, TxHash, PrunableHash | [u8; 32] + +| Table | Key | Value | Description | +|-------------------|----------------------|--------------------|-------------| +| `BlockBlobs` | BlockHeight | `StorableVec<u8>` | Maps a block's height to a serialized byte form of a block +| `BlockHeights` | BlockHash | BlockHeight | Maps a block's hash to its height +| `BlockInfos` | BlockHeight | `BlockInfo` | Contains metadata of all blocks +| `KeyImages` | KeyImage | () | This table is a set with no value, it stores transaction key images +| `NumOutputs` | Amount | u64 | Maps an output's amount to the number of outputs with that amount +| `Outputs` | `PreRctOutputId` | `Output` | This table contains legacy CryptoNote outputs which have clear amounts. This table will not contain an output with 0 amount. +| `PrunedTxBlobs` | TxId | `StorableVec<u8>` | Contains pruned transaction blobs (even if the database is not pruned) +| `PrunableTxBlobs` | TxId | `StorableVec<u8>` | Contains the prunable part of a transaction +| `PrunableHashes` | TxId | PrunableHash | Contains the hash of the prunable part of a transaction +| `RctOutputs` | AmountIndex | `RctOutput` | Contains RingCT outputs mapped from their global RCT index +| `TxBlobs` | TxId | `StorableVec<u8>` | Serialized transaction blobs (bytes) +| `TxIds` | TxHash | TxId | Maps a transaction's hash to its index/ID +| `TxHeights` | TxId | BlockHeight | Maps a transaction's ID to the height of the block it comes from +| `TxOutputs` | TxId | `StorableVec<u64>` | Gives the amount indices of a transaction's outputs +| `TxUnlockTime` | TxId | UnlockTime | Stores the unlock time of a transaction (only if it has a non-zero lock time) + +The definitions for aliases and types (e.g. `RctOutput`) are within the [`cuprate_database::types`](https://github.com/Cuprate/cuprate/blob/31ce89412aa174fc33754f22c9a6d9ef5ddeda28/database/src/types.rs#L51) module. + +<!-- TODO(Boog900): We could split this table again into `RingCT (non-miner) Outputs` and `RingCT (miner) Outputs` as for miner outputs we can store the amount instead of commitment saving 24 bytes per miner output. --> + +### 9.2 Multimap tables +When referencing outputs, Monero will [use the amount and the amount index](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/blockchain_db/lmdb/db_lmdb.cpp#L3447-L3449). This means 2 keys are needed to reach an output. + +With LMDB you can set the `DUP_SORT` flag on a table and then set the key/value to: +```rust +Key = KEY_PART_1 +``` +```rust +Value = { + KEY_PART_2, + VALUE // The actual value we are storing. +} +``` + +Then you can set a custom value sorting function that only takes `KEY_PART_2` into account; this is how `monerod` does it. + +This requires that the underlying database supports: +- multimap tables +- custom sort functions on values +- setting a cursor on a specific key/value + +--- + +Another way to implement this is as follows: +```rust +Key = { KEY_PART_1, KEY_PART_2 } +``` +```rust +Value = VALUE +``` + +Then the key type is simply used to look up the value; this is how `cuprate_database` does it. + +For example, the key/value pair for outputs is: +```rust +PreRctOutputId => Output +``` +where `PreRctOutputId` looks like this: +```rust +struct PreRctOutputId { + amount: u64, + amount_index: u64, +} +``` + +## 10. Known issues and tradeoffs +`cuprate_database` takes many tradeoffs, whether due to: +- Prioritizing certain values over others +- Not having a better solution +- Being "good enough" + +This is a list of the larger ones, along with issues that don't have answers yet. + +### 10.1 Traits abstracting backends +Although all database backends used are very similar, they have some crucial differences in small implementation details that must be worked around when conforming them to `cuprate_database`'s traits. + +Put simply: using `cuprate_database`'s traits is less efficient and more awkward than using the backend directly. + +For example: +- [Data types must be wrapped in compatibility layers when they otherwise wouldn't be](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/backend/heed/env.rs#L101-L116) +- [There are types that only apply to a specific backend, but are visible to all](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/error.rs#L86-L89) +- [There are extra layers of abstraction to smoothen the differences between all backends](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/env.rs#L62-L68) +- [Existing functionality of backends must be taken away, as it isn't supported in the others](https://github.com/Cuprate/cuprate/blob/d0ac94a813e4cd8e0ed8da5e85a53b1d1ace2463/database/src/database.rs#L27-L34) + +This is a _tradeoff_ that `cuprate_database` takes, as: +- The backend itself is usually not the source of bottlenecks in the greater system, as such, small inefficiencies are OK +- None of the lost functionality is crucial for operation +- The ability to use, test, and swap between multiple database backends is [worth it](https://github.com/Cuprate/cuprate/pull/35#issuecomment-1952804393) + +### 10.2 Hot-swappable backends +Using a different backend is really as simple as re-building `cuprate_database` with a different feature flag: +```bash +# Use LMDB. +cargo build --package cuprate-database --features heed + +# Use redb. +cargo build --package cuprate-database --features redb +``` + +This is "good enough" for now, however ideally, this hot-swapping of backends would be able to be done at _runtime_. + +As it is now, `cuprate_database` cannot compile both backends and swap based on user input at runtime; it must be compiled with a certain backend, which will produce a binary with only that backend. + +This also means things like [CI testing multiple backends is awkward](https://github.com/Cuprate/cuprate/blob/main/.github/workflows/ci.yml#L132-L136), as we must re-compile with different feature flags instead. + +### 10.3 Copying unaligned bytes +As mentioned in [`8. (De)serialization`](#8-deserialization), bytes are _copied_ when they are turned into a type `T` due to unaligned bytes being returned from database backends. + +Using a regular reference cast results in an improperly aligned type `T`; [such a type even existing causes undefined behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html). In our case, `bytemuck` saves us by panicking before this occurs. + +Thus, when using `cuprate_database`'s database traits, an _owned_ `T` is returned. + +This is doubly unfortunately for `&[u8]` as this does not even need deserialization. + +For example, `StorableVec` could have been this: +```rust +enum StorableBytes<'a, T: Storable> { + Owned(T), + Ref(&'a T), +} +``` +but this would require supporting types that must be copied regardless with the occasional `&[u8]` that can be returned without casting. This was hard to do so in a generic way, thus all `[u8]`'s are copied and returned as owned `StorableVec`s. + +This is a _tradeoff_ `cuprate_database` takes as: +- `bytemuck::pod_read_unaligned` is cheap enough +- The main API, `service`, needs to return owned value anyway +- Having no references removes a lot of lifetime complexity + +The alternative is either: +- Using proper (de)serialization instead of casting (which comes with its own costs) +- Somehow fixing the alignment issues in the backends mentioned previously + +### 10.4 Endianness +`cuprate_database`'s (de)serialization and storage of bytes are native-endian, as in, byte storage order will depend on the machine it is running on. + +As Cuprate's build-targets are all little-endian ([big-endian by default machines barely exist](https://en.wikipedia.org/wiki/Endianness#Hardware)), this doesn't matter much and the byte ordering can be seen as a constant. + +Practically, this means `cuprated`'s database files can be transferred across computers, as can `monerod`'s. + +### 10.5 Extra table data +Some of `cuprate_database`'s tables differ from `monerod`'s tables, for example, the way [`9.2 Multimap tables`](#92-multimap-tables) tables are done requires that the primary key is stored _for all_ entries, compared to `monerod` only needing to store it once. + +For example: +```rust +// `monerod` only stores `amount: 1` once, +// `cuprated` stores it each time it appears. +struct PreRctOutputId { amount: 1, amount_index: 0 } +struct PreRctOutputId { amount: 1, amount_index: 1 } +``` + +This means `cuprated`'s database will be slightly larger than `monerod`'s. + +The current method `cuprate_database` uses will be "good enough" until usage shows that it must be optimized as multimap tables are tricky to implement across all backends. \ No newline at end of file diff --git a/storage/database/src/backend/heed/database.rs b/storage/database/src/backend/heed/database.rs new file mode 100644 index 00000000..c985d0de --- /dev/null +++ b/storage/database/src/backend/heed/database.rs @@ -0,0 +1,261 @@ +//! Implementation of `trait Database` for `heed`. + +//---------------------------------------------------------------------------------------------------- Import +use std::{cell::RefCell, ops::RangeBounds}; + +use crate::{ + backend::heed::types::HeedDb, + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + error::RuntimeError, + table::Table, +}; + +//---------------------------------------------------------------------------------------------------- Heed Database Wrappers +// Q. Why does `HeedTableR{o,w}` exist? +// A. These wrapper types combine `heed`'s database/table +// types with its transaction types. It exists to match +// `redb`, which has this behavior built-in. +// +// `redb` forces us to abstract read/write semantics +// at the _opened table_ level, so, we must match that in `heed`, +// which abstracts it at the transaction level. +// +// We must also maintain the ability for +// write operations to also read, aka, `Rw`. + +/// An opened read-only database associated with a transaction. +/// +/// Matches `redb::ReadOnlyTable`. +pub(super) struct HeedTableRo<'tx, T: Table> { + /// An already opened database table. + pub(super) db: HeedDb<T::Key, T::Value>, + /// The associated read-only transaction that opened this table. + pub(super) tx_ro: &'tx heed::RoTxn<'tx>, +} + +/// An opened read/write database associated with a transaction. +/// +/// Matches `redb::Table` (read & write). +pub(super) struct HeedTableRw<'env, 'tx, T: Table> { + /// An already opened database table. + pub(super) db: HeedDb<T::Key, T::Value>, + /// The associated read/write transaction that opened this table. + pub(super) tx_rw: &'tx RefCell<heed::RwTxn<'env>>, +} + +//---------------------------------------------------------------------------------------------------- Shared functions +// FIXME: we cannot just deref `HeedTableRw -> HeedTableRo` and +// call the functions since the database is held by value, so +// just use these generic functions that both can call instead. + +/// Shared [`DatabaseRo::get()`]. +#[inline] +fn get<T: Table>( + db: &HeedDb<T::Key, T::Value>, + tx_ro: &heed::RoTxn<'_>, + key: &T::Key, +) -> Result<T::Value, RuntimeError> { + db.get(tx_ro, key)?.ok_or(RuntimeError::KeyNotFound) +} + +/// Shared [`DatabaseRo::len()`]. +#[inline] +fn len<T: Table>( + db: &HeedDb<T::Key, T::Value>, + tx_ro: &heed::RoTxn<'_>, +) -> Result<u64, RuntimeError> { + Ok(db.len(tx_ro)?) +} + +/// Shared [`DatabaseRo::first()`]. +#[inline] +fn first<T: Table>( + db: &HeedDb<T::Key, T::Value>, + tx_ro: &heed::RoTxn<'_>, +) -> Result<(T::Key, T::Value), RuntimeError> { + db.first(tx_ro)?.ok_or(RuntimeError::KeyNotFound) +} + +/// Shared [`DatabaseRo::last()`]. +#[inline] +fn last<T: Table>( + db: &HeedDb<T::Key, T::Value>, + tx_ro: &heed::RoTxn<'_>, +) -> Result<(T::Key, T::Value), RuntimeError> { + db.last(tx_ro)?.ok_or(RuntimeError::KeyNotFound) +} + +/// Shared [`DatabaseRo::is_empty()`]. +#[inline] +fn is_empty<T: Table>( + db: &HeedDb<T::Key, T::Value>, + tx_ro: &heed::RoTxn<'_>, +) -> Result<bool, RuntimeError> { + Ok(db.is_empty(tx_ro)?) +} + +//---------------------------------------------------------------------------------------------------- DatabaseIter Impl +impl<T: Table> DatabaseIter<T> for HeedTableRo<'_, T> { + #[inline] + fn get_range<'a, Range>( + &'a self, + range: Range, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + 'a, RuntimeError> + where + Range: RangeBounds<T::Key> + 'a, + { + Ok(self.db.range(self.tx_ro, &range)?.map(|res| Ok(res?.1))) + } + + #[inline] + fn iter( + &self, + ) -> Result<impl Iterator<Item = Result<(T::Key, T::Value), RuntimeError>> + '_, RuntimeError> + { + Ok(self.db.iter(self.tx_ro)?.map(|res| Ok(res?))) + } + + #[inline] + fn keys( + &self, + ) -> Result<impl Iterator<Item = Result<T::Key, RuntimeError>> + '_, RuntimeError> { + Ok(self.db.iter(self.tx_ro)?.map(|res| Ok(res?.0))) + } + + #[inline] + fn values( + &self, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + '_, RuntimeError> { + Ok(self.db.iter(self.tx_ro)?.map(|res| Ok(res?.1))) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseRo Impl +// SAFETY: `HeedTableRo: !Send` as it holds a reference to `heed::RoTxn: Send + !Sync`. +unsafe impl<T: Table> DatabaseRo<T> for HeedTableRo<'_, T> { + #[inline] + fn get(&self, key: &T::Key) -> Result<T::Value, RuntimeError> { + get::<T>(&self.db, self.tx_ro, key) + } + + #[inline] + fn len(&self) -> Result<u64, RuntimeError> { + len::<T>(&self.db, self.tx_ro) + } + + #[inline] + fn first(&self) -> Result<(T::Key, T::Value), RuntimeError> { + first::<T>(&self.db, self.tx_ro) + } + + #[inline] + fn last(&self) -> Result<(T::Key, T::Value), RuntimeError> { + last::<T>(&self.db, self.tx_ro) + } + + #[inline] + fn is_empty(&self) -> Result<bool, RuntimeError> { + is_empty::<T>(&self.db, self.tx_ro) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseRw Impl +// SAFETY: The `Send` bound only applies to `HeedTableRo`. +// `HeedTableRw`'s write transaction is `!Send`. +unsafe impl<T: Table> DatabaseRo<T> for HeedTableRw<'_, '_, T> { + #[inline] + fn get(&self, key: &T::Key) -> Result<T::Value, RuntimeError> { + get::<T>(&self.db, &self.tx_rw.borrow(), key) + } + + #[inline] + fn len(&self) -> Result<u64, RuntimeError> { + len::<T>(&self.db, &self.tx_rw.borrow()) + } + + #[inline] + fn first(&self) -> Result<(T::Key, T::Value), RuntimeError> { + first::<T>(&self.db, &self.tx_rw.borrow()) + } + + #[inline] + fn last(&self) -> Result<(T::Key, T::Value), RuntimeError> { + last::<T>(&self.db, &self.tx_rw.borrow()) + } + + #[inline] + fn is_empty(&self) -> Result<bool, RuntimeError> { + is_empty::<T>(&self.db, &self.tx_rw.borrow()) + } +} + +impl<T: Table> DatabaseRw<T> for HeedTableRw<'_, '_, T> { + #[inline] + fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> { + Ok(self.db.put(&mut self.tx_rw.borrow_mut(), key, value)?) + } + + #[inline] + fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> { + self.db.delete(&mut self.tx_rw.borrow_mut(), key)?; + Ok(()) + } + + #[inline] + fn take(&mut self, key: &T::Key) -> Result<T::Value, RuntimeError> { + // LMDB/heed does not return the value on deletion. + // So, fetch it first - then delete. + let value = get::<T>(&self.db, &self.tx_rw.borrow(), key)?; + match self.db.delete(&mut self.tx_rw.borrow_mut(), key) { + Ok(true) => Ok(value), + Err(e) => Err(e.into()), + // We just `get()`'ed the value - it is + // incorrect for it to suddenly not exist. + Ok(false) => unreachable!(), + } + } + + #[inline] + fn pop_first(&mut self) -> Result<(T::Key, T::Value), RuntimeError> { + let tx_rw = &mut self.tx_rw.borrow_mut(); + + // Get the value first... + let Some((key, value)) = self.db.first(tx_rw)? else { + return Err(RuntimeError::KeyNotFound); + }; + + // ...then remove it. + match self.db.delete(tx_rw, &key) { + Ok(true) => Ok((key, value)), + Err(e) => Err(e.into()), + // We just `get()`'ed the value - it is + // incorrect for it to suddenly not exist. + Ok(false) => unreachable!(), + } + } + + #[inline] + fn pop_last(&mut self) -> Result<(T::Key, T::Value), RuntimeError> { + let tx_rw = &mut self.tx_rw.borrow_mut(); + + // Get the value first... + let Some((key, value)) = self.db.last(tx_rw)? else { + return Err(RuntimeError::KeyNotFound); + }; + + // ...then remove it. + match self.db.delete(tx_rw, &key) { + Ok(true) => Ok((key, value)), + Err(e) => Err(e.into()), + // We just `get()`'ed the value - it is + // incorrect for it to suddenly not exist. + Ok(false) => unreachable!(), + } + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/heed/env.rs b/storage/database/src/backend/heed/env.rs new file mode 100644 index 00000000..56064849 --- /dev/null +++ b/storage/database/src/backend/heed/env.rs @@ -0,0 +1,347 @@ +//! Implementation of `trait Env` for `heed`. + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + cell::RefCell, + num::NonZeroUsize, + sync::{RwLock, RwLockReadGuard}, +}; + +use heed::{DatabaseOpenOptions, EnvFlags, EnvOpenOptions}; + +use crate::{ + backend::heed::{ + database::{HeedTableRo, HeedTableRw}, + storable::StorableHeed, + types::HeedDb, + }, + config::{Config, SyncMode}, + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + env::{Env, EnvInner}, + error::{InitError, RuntimeError}, + resize::ResizeAlgorithm, + table::Table, + tables::call_fn_on_all_tables_or_early_return, +}; + +//---------------------------------------------------------------------------------------------------- Consts +/// Panic message when there's a table missing. +const PANIC_MSG_MISSING_TABLE: &str = + "cuprate_database::Env should uphold the invariant that all tables are already created"; + +//---------------------------------------------------------------------------------------------------- ConcreteEnv +/// A strongly typed, concrete database environment, backed by `heed`. +pub struct ConcreteEnv { + /// The actual database environment. + /// + /// # Why `RwLock`? + /// We need mutual exclusive access to the environment for resizing. + /// + /// Using 2 atomics for mutual exclusion was considered: + /// - `currently_resizing: AtomicBool` + /// - `reader_count: AtomicUsize` + /// + /// This is how `monerod` does it: + /// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L354-L355> + /// + /// `currently_resizing` would be set to `true` on resizes and + /// `reader_count` would be spinned on until 0, at which point + /// we are safe to resize. + /// + /// Although, 3 atomic operations (check atomic bool, `reader_count++`, `reader_count--`) + /// turns out to be roughly as expensive as acquiring a non-contended `RwLock`, + /// the CPU sleeping instead of spinning is much better too. + /// + /// # `unwrap()` + /// This will be [`unwrap()`]ed everywhere. + /// + /// If lock is poisoned, we want all of Cuprate to panic. + env: RwLock<heed::Env>, + + /// The configuration we were opened with + /// (and in current use). + pub(super) config: Config, +} + +impl Drop for ConcreteEnv { + fn drop(&mut self) { + // INVARIANT: drop(ConcreteEnv) must sync. + // + // SOMEDAY: + // "if the environment has the MDB_NOSYNC flag set the flushes will be omitted, + // and with MDB_MAPASYNC they will be asynchronous." + // <http://www.lmdb.tech/doc/group__mdb.html#ga85e61f05aa68b520cc6c3b981dba5037> + // + // We need to do `mdb_env_set_flags(&env, MDB_NOSYNC|MDB_ASYNCMAP, 0)` + // to clear the no sync and async flags such that the below `self.sync()` + // _actually_ synchronously syncs. + if let Err(_e) = crate::Env::sync(self) { + // TODO: log error? + } + + // TODO: log that we are dropping the database. + + // TODO: use tracing. + // <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/lmdb.h#L49-L61> + let result = self.env.read().unwrap().clear_stale_readers(); + match result { + Ok(n) => println!("LMDB stale readers cleared: {n}"), + Err(e) => println!("LMDB stale reader clear error: {e:?}"), + } + } +} + +//---------------------------------------------------------------------------------------------------- Env Impl +impl Env for ConcreteEnv { + const MANUAL_RESIZE: bool = true; + const SYNCS_PER_TX: bool = false; + type EnvInner<'env> = RwLockReadGuard<'env, heed::Env>; + type TxRo<'tx> = heed::RoTxn<'tx>; + + /// HACK: + /// `heed::RwTxn` is wrapped in `RefCell` to allow: + /// - opening a database with only a `&` to it + /// - allowing 1 write tx to open multiple tables + /// + /// Our mutable accesses are safe and will not panic as: + /// - Write transactions are `!Sync` + /// - A table operation does not hold a reference to the inner cell + /// once the call is over + /// - The function to manipulate the table takes the same type + /// of reference that the `RefCell` gets for that function + /// + /// Also see: + /// - <https://github.com/Cuprate/cuprate/pull/102#discussion_r1548695610> + /// - <https://github.com/Cuprate/cuprate/pull/104> + type TxRw<'tx> = RefCell<heed::RwTxn<'tx>>; + + #[cold] + #[inline(never)] // called once. + fn open(config: Config) -> Result<Self, InitError> { + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324> + + let mut env_open_options = EnvOpenOptions::new(); + + // Map our `Config` sync mode to the LMDB environment flags. + // + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324> + let flags = match config.sync_mode { + SyncMode::Safe => EnvFlags::empty(), + SyncMode::Async => EnvFlags::MAP_ASYNC, + SyncMode::Fast => EnvFlags::NO_SYNC | EnvFlags::WRITE_MAP | EnvFlags::MAP_ASYNC, + // SOMEDAY: dynamic syncs are not implemented. + SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(), + }; + + // SAFETY: the flags we're setting are 'unsafe' + // from a data durability perspective, although, + // the user config wanted this. + // + // MAYBE: We may need to open/create tables with certain flags + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324> + // MAYBE: Set comparison functions for certain tables + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324> + unsafe { + env_open_options.flags(flags); + } + + // Set the memory map size to + // (current disk size) + (a bit of leeway) + // to account for empty databases where we + // need to write same tables. + #[allow(clippy::cast_possible_truncation)] // only 64-bit targets + let disk_size_bytes = match std::fs::File::open(&config.db_file) { + Ok(file) => file.metadata()?.len() as usize, + // The database file doesn't exist, 0 bytes. + Err(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => 0, + Err(io_err) => return Err(io_err.into()), + }; + // Add leeway space. + let memory_map_size = crate::resize::fixed_bytes(disk_size_bytes, 1_000_000 /* 1MB */); + env_open_options.map_size(memory_map_size.get()); + + // Set the max amount of database tables. + // We know at compile time how many tables there are. + // SOMEDAY: ...how many? + env_open_options.max_dbs(32); + + // LMDB documentation: + // ``` + // Number of slots in the reader table. + // This value was chosen somewhat arbitrarily. 126 readers plus a + // couple mutexes fit exactly into 8KB on my development machine. + // ``` + // <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L794-L799> + // + // So, we're going to be following these rules: + // - Use at least 126 reader threads + // - Add 16 extra reader threads if <126 + // + // FIXME: This behavior is from `monerod`: + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324> + // I believe this could be adjusted percentage-wise so very high + // thread PCs can benefit from something like (cuprated + anything that uses the DB in the future). + // For now: + // - No other program using our DB exists + // - Almost no-one has a 126+ thread CPU + let reader_threads = + u32::try_from(config.reader_threads.as_threads().get()).unwrap_or(u32::MAX); + env_open_options.max_readers(if reader_threads < 110 { + 126 + } else { + reader_threads.saturating_add(16) + }); + + // Create the database directory if it doesn't exist. + std::fs::create_dir_all(config.db_directory())?; + // Open the environment in the user's PATH. + // SAFETY: LMDB uses a memory-map backed file. + // <https://docs.rs/heed/0.20.0/heed/struct.EnvOpenOptions.html#method.open> + let env = unsafe { env_open_options.open(config.db_directory())? }; + + /// Function that creates the tables based off the passed `T: Table`. + fn create_table<T: Table>( + env: &heed::Env, + tx_rw: &mut heed::RwTxn<'_>, + ) -> Result<(), InitError> { + DatabaseOpenOptions::new(env) + .name(<T as Table>::NAME) + .types::<StorableHeed<<T as Table>::Key>, StorableHeed<<T as Table>::Value>>() + .create(tx_rw)?; + Ok(()) + } + + let mut tx_rw = env.write_txn()?; + // Create all tables. + // FIXME: this macro is kinda awkward. + { + let env = &env; + let tx_rw = &mut tx_rw; + match call_fn_on_all_tables_or_early_return!(create_table(env, tx_rw)) { + Ok(_) => (), + Err(e) => return Err(e), + } + } + + // INVARIANT: this should never return `ResizeNeeded` due to adding + // some tables since we added some leeway to the memory map above. + tx_rw.commit()?; + + Ok(Self { + env: RwLock::new(env), + config, + }) + } + + fn config(&self) -> &Config { + &self.config + } + + fn sync(&self) -> Result<(), RuntimeError> { + Ok(self.env.read().unwrap().force_sync()?) + } + + fn resize_map(&self, resize_algorithm: Option<ResizeAlgorithm>) -> NonZeroUsize { + let resize_algorithm = resize_algorithm.unwrap_or_else(|| self.config().resize_algorithm); + + let current_size_bytes = self.current_map_size(); + let new_size_bytes = resize_algorithm.resize(current_size_bytes); + + // SAFETY: + // Resizing requires that we have + // exclusive access to the database environment. + // Our `heed::Env` is wrapped within a `RwLock`, + // and we have a WriteGuard to it, so we're safe. + // + // <http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5> + unsafe { + // INVARIANT: `resize()` returns a valid `usize` to resize to. + self.env + .write() + .unwrap() + .resize(new_size_bytes.get()) + .unwrap(); + } + + new_size_bytes + } + + #[inline] + fn current_map_size(&self) -> usize { + self.env.read().unwrap().info().map_size + } + + #[inline] + fn env_inner(&self) -> Self::EnvInner<'_> { + self.env.read().unwrap() + } +} + +//---------------------------------------------------------------------------------------------------- EnvInner Impl +impl<'env> EnvInner<'env, heed::RoTxn<'env>, RefCell<heed::RwTxn<'env>>> + for RwLockReadGuard<'env, heed::Env> +where + Self: 'env, +{ + #[inline] + fn tx_ro(&'env self) -> Result<heed::RoTxn<'env>, RuntimeError> { + Ok(self.read_txn()?) + } + + #[inline] + fn tx_rw(&'env self) -> Result<RefCell<heed::RwTxn<'env>>, RuntimeError> { + Ok(RefCell::new(self.write_txn()?)) + } + + #[inline] + fn open_db_ro<T: Table>( + &self, + tx_ro: &heed::RoTxn<'env>, + ) -> Result<impl DatabaseRo<T> + DatabaseIter<T>, RuntimeError> { + // Open up a read-only database using our table's const metadata. + Ok(HeedTableRo { + db: self + .open_database(tx_ro, Some(T::NAME))? + .expect(PANIC_MSG_MISSING_TABLE), + tx_ro, + }) + } + + #[inline] + fn open_db_rw<T: Table>( + &self, + tx_rw: &RefCell<heed::RwTxn<'env>>, + ) -> Result<impl DatabaseRw<T>, RuntimeError> { + let tx_ro = tx_rw.borrow(); + + // Open up a read/write database using our table's const metadata. + Ok(HeedTableRw { + db: self + .open_database(&tx_ro, Some(T::NAME))? + .expect(PANIC_MSG_MISSING_TABLE), + tx_rw, + }) + } + + #[inline] + fn clear_db<T: Table>( + &self, + tx_rw: &mut RefCell<heed::RwTxn<'env>>, + ) -> Result<(), RuntimeError> { + let tx_rw = tx_rw.get_mut(); + + // Open the table first... + let db: HeedDb<T::Key, T::Value> = self + .open_database(tx_rw, Some(T::NAME))? + .expect(PANIC_MSG_MISSING_TABLE); + + // ...then clear it. + Ok(db.clear(tx_rw)?) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/heed/error.rs b/storage/database/src/backend/heed/error.rs new file mode 100644 index 00000000..c47bd908 --- /dev/null +++ b/storage/database/src/backend/heed/error.rs @@ -0,0 +1,152 @@ +//! Conversion from `heed::Error` -> `cuprate_database`'s errors. + +//---------------------------------------------------------------------------------------------------- Use +use crate::constants::DATABASE_CORRUPT_MSG; + +//---------------------------------------------------------------------------------------------------- InitError +impl From<heed::Error> for crate::InitError { + fn from(error: heed::Error) -> Self { + use heed::Error as E1; + use heed::MdbError as E2; + + // Reference of all possible errors `heed` will return + // upon using [`heed::EnvOpenOptions::open`]: + // <https://docs.rs/heed/latest/src/heed/env.rs.html#149-219> + match error { + E1::Io(io_error) => Self::Io(io_error), + E1::DatabaseClosing => Self::ShuttingDown, + + // LMDB errors. + E1::Mdb(mdb_error) => match mdb_error { + E2::Invalid => Self::Invalid, + E2::VersionMismatch => Self::InvalidVersion, + + // "Located page was wrong type". + // <https://docs.rs/heed/latest/heed/enum.MdbError.html#variant.Corrupted> + // + // "Requested page not found - this usually indicates corruption." + // <https://docs.rs/heed/latest/heed/enum.MdbError.html#variant.PageNotFound> + E2::Corrupted | E2::PageNotFound => Self::Corrupt, + + // These errors shouldn't be returned on database init. + E2::Incompatible + | E2::Other(_) + | E2::BadTxn + | E2::Problem + | E2::KeyExist + | E2::NotFound + | E2::MapFull + | E2::ReadersFull + | E2::PageFull + | E2::DbsFull + | E2::TlsFull + | E2::TxnFull + | E2::CursorFull + | E2::MapResized + | E2::BadRslot + | E2::BadValSize + | E2::BadDbi + | E2::Panic => Self::Unknown(Box::new(mdb_error)), + }, + + E1::BadOpenOptions { .. } | E1::Encoding(_) | E1::Decoding(_) => { + Self::Unknown(Box::new(error)) + } + } + } +} + +//---------------------------------------------------------------------------------------------------- RuntimeError +#[allow(clippy::fallible_impl_from)] // We need to panic sometimes. +impl From<heed::Error> for crate::RuntimeError { + /// # Panics + /// This will panic on unrecoverable errors for safety. + fn from(error: heed::Error) -> Self { + use heed::Error as E1; + use heed::MdbError as E2; + + match error { + // I/O errors. + E1::Io(io_error) => Self::Io(io_error), + + // LMDB errors. + E1::Mdb(mdb_error) => match mdb_error { + E2::KeyExist => Self::KeyExists, + E2::NotFound => Self::KeyNotFound, + E2::MapFull => Self::ResizeNeeded, + + // Corruption errors, these have special panic messages. + // + // "Located page was wrong type". + // <https://docs.rs/heed/latest/heed/enum.MdbError.html#variant.Corrupted> + // + // "Requested page not found - this usually indicates corruption." + // <https://docs.rs/heed/latest/heed/enum.MdbError.html#variant.PageNotFound> + E2::Corrupted | E2::PageNotFound => panic!("{mdb_error:#?}\n{DATABASE_CORRUPT_MSG}"), + + // These errors should not occur, and if they do, + // the best thing `cuprate_database` can do for + // safety is to panic right here. + E2::Panic + | E2::PageFull + | E2::Other(_) + | E2::BadTxn + | E2::Problem + | E2::Invalid + | E2::TlsFull + | E2::TxnFull + | E2::BadRslot + | E2::VersionMismatch + | E2::BadDbi => panic!("{mdb_error:#?}"), + + // These errors are the same as above, but instead + // of being errors we can't control, these are errors + // that only happen if we write incorrect code. + + // "Database contents grew beyond environment mapsize." + // We should be resizing the map when needed, this error + // occurring indicates we did _not_ do that, which is a bug + // and we should panic. + // + // FIXME: This can also mean _another_ process wrote to our + // LMDB file and increased the size. I don't think we need to accommodate for this. + // <http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5> + // Although `monerod` reacts to that instead of `MDB_MAP_FULL` + // which is what `mdb_put()` returns so... idk? + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L526> + | E2::MapResized + // We should be setting `heed::EnvOpenOptions::max_readers()` + // with our reader thread value in [`crate::config::Config`], + // thus this error should never occur. + // <http://www.lmdb.tech/doc/group__mdb.html#gae687966c24b790630be2a41573fe40e2> + | E2::ReadersFull + // Do not open more database tables than we initially started with. + // We know this number at compile time (amount of `Table`'s) so this + // should never happen. + // <https://docs.rs/heed/0.20.0-alpha.9/heed/struct.EnvOpenOptions.html#method.max_dbs> + // <https://docs.rs/heed/0.20.0-alpha.9/src/heed/env.rs.html#251> + | E2::DbsFull + // Don't do crazy multi-nested LMDB cursor stuff. + | E2::CursorFull + // <https://docs.rs/heed/0.20.0-alpha.9/heed/enum.MdbError.html#variant.Incompatible> + | E2::Incompatible + // Unsupported size of key/DB name/data, or wrong DUP_FIXED size. + // Don't use a key that is `>511` bytes. + // <http://www.lmdb.tech/doc/group__mdb.html#gaaf0be004f33828bf2fb09d77eb3cef94> + | E2::BadValSize + => panic!("fix the database code! {mdb_error:#?}"), + }, + + // Only if we write incorrect code. + E1::DatabaseClosing | E1::BadOpenOptions { .. } | E1::Encoding(_) | E1::Decoding(_) => { + panic!("fix the database code! {error:#?}") + } + } + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/heed/mod.rs b/storage/database/src/backend/heed/mod.rs new file mode 100644 index 00000000..8bfae718 --- /dev/null +++ b/storage/database/src/backend/heed/mod.rs @@ -0,0 +1,10 @@ +//! Database backend implementation backed by `heed`. + +mod env; +pub use env::ConcreteEnv; + +mod database; +mod error; +mod storable; +mod transaction; +mod types; diff --git a/storage/database/src/backend/heed/storable.rs b/storage/database/src/backend/heed/storable.rs new file mode 100644 index 00000000..83442212 --- /dev/null +++ b/storage/database/src/backend/heed/storable.rs @@ -0,0 +1,122 @@ +//! `cuprate_database::Storable` <-> `heed` serde trait compatibility layer. + +//---------------------------------------------------------------------------------------------------- Use +use std::{borrow::Cow, marker::PhantomData}; + +use heed::{BoxedError, BytesDecode, BytesEncode}; + +use crate::storable::Storable; + +//---------------------------------------------------------------------------------------------------- StorableHeed +/// The glue struct that implements `heed`'s (de)serialization +/// traits on any type that implements `cuprate_database::Storable`. +/// +/// Never actually gets constructed, just used for trait bound translations. +pub(super) struct StorableHeed<T>(PhantomData<T>) +where + T: Storable + ?Sized; + +//---------------------------------------------------------------------------------------------------- BytesDecode +impl<'a, T> BytesDecode<'a> for StorableHeed<T> +where + T: Storable + 'static, +{ + type DItem = T; + + #[inline] + /// This function is infallible (will always return `Ok`). + fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, BoxedError> { + Ok(T::from_bytes(bytes)) + } +} + +//---------------------------------------------------------------------------------------------------- BytesEncode +impl<'a, T> BytesEncode<'a> for StorableHeed<T> +where + T: Storable + ?Sized + 'a, +{ + type EItem = T; + + #[inline] + /// This function is infallible (will always return `Ok`). + fn bytes_encode(item: &'a Self::EItem) -> Result<Cow<'a, [u8]>, BoxedError> { + Ok(Cow::Borrowed(item.as_bytes())) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use std::fmt::Debug; + + use super::*; + use crate::{StorableBytes, StorableVec}; + + // Each `#[test]` function has a `test()` to: + // - log + // - simplify trait bounds + // - make sure the right function is being called + + #[test] + /// Assert `BytesEncode::bytes_encode` is accurate. + fn bytes_encode() { + fn test<T>(t: &T, expected: &[u8]) + where + T: Storable + ?Sized, + { + println!("t: {t:?}, expected: {expected:?}"); + assert_eq!( + <StorableHeed::<T> as BytesEncode>::bytes_encode(t).unwrap(), + expected + ); + } + + test::<()>(&(), &[]); + test::<u8>(&0, &[0]); + test::<u16>(&1, &[1, 0]); + test::<u32>(&2, &[2, 0, 0, 0]); + test::<u64>(&3, &[3, 0, 0, 0, 0, 0, 0, 0]); + test::<i8>(&-1, &[255]); + test::<i16>(&-2, &[254, 255]); + test::<i32>(&-3, &[253, 255, 255, 255]); + test::<i64>(&-4, &[252, 255, 255, 255, 255, 255, 255, 255]); + test::<StorableVec<u8>>(&StorableVec(vec![1, 2]), &[1, 2]); + test::<StorableBytes>(&StorableBytes(bytes::Bytes::from_static(&[1, 2])), &[1, 2]); + test::<[u8; 0]>(&[], &[]); + test::<[u8; 1]>(&[255], &[255]); + test::<[u8; 2]>(&[111, 0], &[111, 0]); + test::<[u8; 3]>(&[1, 0, 1], &[1, 0, 1]); + } + + #[test] + /// Assert `BytesDecode::bytes_decode` is accurate. + fn bytes_decode() { + fn test<T>(bytes: &[u8], expected: &T) + where + T: Storable + PartialEq + ToOwned + Debug + 'static, + T::Owned: Debug, + { + println!("bytes: {bytes:?}, expected: {expected:?}"); + assert_eq!( + &<StorableHeed::<T> as BytesDecode>::bytes_decode(bytes).unwrap(), + expected + ); + } + + test::<()>([].as_slice(), &()); + test::<u8>([0].as_slice(), &0); + test::<u16>([1, 0].as_slice(), &1); + test::<u32>([2, 0, 0, 0].as_slice(), &2); + test::<u64>([3, 0, 0, 0, 0, 0, 0, 0].as_slice(), &3); + test::<i8>([255].as_slice(), &-1); + test::<i16>([254, 255].as_slice(), &-2); + test::<i32>([253, 255, 255, 255].as_slice(), &-3); + test::<i64>([252, 255, 255, 255, 255, 255, 255, 255].as_slice(), &-4); + test::<StorableVec<u8>>(&[1, 2], &StorableVec(vec![1, 2])); + test::<StorableBytes>(&[1, 2], &StorableBytes(bytes::Bytes::from_static(&[1, 2]))); + test::<[u8; 0]>([].as_slice(), &[]); + test::<[u8; 1]>([255].as_slice(), &[255]); + test::<[u8; 2]>([111, 0].as_slice(), &[111, 0]); + test::<[u8; 3]>([1, 0, 1].as_slice(), &[1, 0, 1]); + } +} diff --git a/storage/database/src/backend/heed/transaction.rs b/storage/database/src/backend/heed/transaction.rs new file mode 100644 index 00000000..d32f3707 --- /dev/null +++ b/storage/database/src/backend/heed/transaction.rs @@ -0,0 +1,41 @@ +//! Implementation of `trait TxRo/TxRw` for `heed`. + +use std::cell::RefCell; + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + error::RuntimeError, + transaction::{TxRo, TxRw}, +}; + +//---------------------------------------------------------------------------------------------------- TxRo +impl TxRo<'_> for heed::RoTxn<'_> { + fn commit(self) -> Result<(), RuntimeError> { + Ok(heed::RoTxn::commit(self)?) + } +} + +//---------------------------------------------------------------------------------------------------- TxRw +impl TxRo<'_> for RefCell<heed::RwTxn<'_>> { + fn commit(self) -> Result<(), RuntimeError> { + TxRw::commit(self) + } +} + +impl TxRw<'_> for RefCell<heed::RwTxn<'_>> { + fn commit(self) -> Result<(), RuntimeError> { + Ok(heed::RwTxn::commit(self.into_inner())?) + } + + /// This function is infallible. + fn abort(self) -> Result<(), RuntimeError> { + heed::RwTxn::abort(self.into_inner()); + Ok(()) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/heed/types.rs b/storage/database/src/backend/heed/types.rs new file mode 100644 index 00000000..6a99d0df --- /dev/null +++ b/storage/database/src/backend/heed/types.rs @@ -0,0 +1,8 @@ +//! `heed` type aliases. + +//---------------------------------------------------------------------------------------------------- Use +use crate::backend::heed::storable::StorableHeed; + +//---------------------------------------------------------------------------------------------------- Types +/// The concrete database type for `heed`, usable for reads and writes. +pub(super) type HeedDb<K, V> = heed::Database<StorableHeed<K>, StorableHeed<V>>; diff --git a/storage/database/src/backend/mod.rs b/storage/database/src/backend/mod.rs new file mode 100644 index 00000000..11ae40b8 --- /dev/null +++ b/storage/database/src/backend/mod.rs @@ -0,0 +1,16 @@ +//! Database backends. + +cfg_if::cfg_if! { + // If both backends are enabled, fallback to `heed`. + // This is useful when using `--all-features`. + if #[cfg(all(feature = "redb", not(feature = "heed")))] { + mod redb; + pub use redb::ConcreteEnv; + } else { + mod heed; + pub use heed::ConcreteEnv; + } +} + +#[cfg(test)] +mod tests; diff --git a/storage/database/src/backend/redb/database.rs b/storage/database/src/backend/redb/database.rs new file mode 100644 index 00000000..cd9a0be9 --- /dev/null +++ b/storage/database/src/backend/redb/database.rs @@ -0,0 +1,213 @@ +//! Implementation of `trait DatabaseR{o,w}` for `redb`. + +//---------------------------------------------------------------------------------------------------- Import +use std::ops::RangeBounds; + +use redb::ReadableTable; + +use crate::{ + backend::redb::{ + storable::StorableRedb, + types::{RedbTableRo, RedbTableRw}, + }, + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + error::RuntimeError, + table::Table, +}; + +//---------------------------------------------------------------------------------------------------- Shared functions +// FIXME: we cannot just deref `RedbTableRw -> RedbTableRo` and +// call the functions since the database is held by value, so +// just use these generic functions that both can call instead. + +/// Shared [`DatabaseRo::get()`]. +#[inline] +fn get<T: Table + 'static>( + db: &impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>, + key: &T::Key, +) -> Result<T::Value, RuntimeError> { + Ok(db.get(key)?.ok_or(RuntimeError::KeyNotFound)?.value()) +} + +/// Shared [`DatabaseRo::len()`]. +#[inline] +fn len<T: Table>( + db: &impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>, +) -> Result<u64, RuntimeError> { + Ok(db.len()?) +} + +/// Shared [`DatabaseRo::first()`]. +#[inline] +fn first<T: Table>( + db: &impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>, +) -> Result<(T::Key, T::Value), RuntimeError> { + let (key, value) = db.first()?.ok_or(RuntimeError::KeyNotFound)?; + Ok((key.value(), value.value())) +} + +/// Shared [`DatabaseRo::last()`]. +#[inline] +fn last<T: Table>( + db: &impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>, +) -> Result<(T::Key, T::Value), RuntimeError> { + let (key, value) = db.last()?.ok_or(RuntimeError::KeyNotFound)?; + Ok((key.value(), value.value())) +} + +/// Shared [`DatabaseRo::is_empty()`]. +#[inline] +fn is_empty<T: Table>( + db: &impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>, +) -> Result<bool, RuntimeError> { + Ok(db.is_empty()?) +} + +//---------------------------------------------------------------------------------------------------- DatabaseIter +impl<T: Table + 'static> DatabaseIter<T> for RedbTableRo<T::Key, T::Value> { + #[inline] + fn get_range<'a, Range>( + &'a self, + range: Range, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + 'a, RuntimeError> + where + Range: RangeBounds<T::Key> + 'a, + { + Ok(ReadableTable::range(self, range)?.map(|result| { + let (_key, value) = result?; + Ok(value.value()) + })) + } + + #[inline] + fn iter( + &self, + ) -> Result<impl Iterator<Item = Result<(T::Key, T::Value), RuntimeError>> + '_, RuntimeError> + { + Ok(ReadableTable::iter(self)?.map(|result| { + let (key, value) = result?; + Ok((key.value(), value.value())) + })) + } + + #[inline] + fn keys( + &self, + ) -> Result<impl Iterator<Item = Result<T::Key, RuntimeError>> + '_, RuntimeError> { + Ok(ReadableTable::iter(self)?.map(|result| { + let (key, _value) = result?; + Ok(key.value()) + })) + } + + #[inline] + fn values( + &self, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + '_, RuntimeError> { + Ok(ReadableTable::iter(self)?.map(|result| { + let (_key, value) = result?; + Ok(value.value()) + })) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseRo +// SAFETY: Both `redb`'s transaction and table types are `Send + Sync`. +unsafe impl<T: Table + 'static> DatabaseRo<T> for RedbTableRo<T::Key, T::Value> { + #[inline] + fn get(&self, key: &T::Key) -> Result<T::Value, RuntimeError> { + get::<T>(self, key) + } + + #[inline] + fn len(&self) -> Result<u64, RuntimeError> { + len::<T>(self) + } + + #[inline] + fn first(&self) -> Result<(T::Key, T::Value), RuntimeError> { + first::<T>(self) + } + + #[inline] + fn last(&self) -> Result<(T::Key, T::Value), RuntimeError> { + last::<T>(self) + } + + #[inline] + fn is_empty(&self) -> Result<bool, RuntimeError> { + is_empty::<T>(self) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseRw +// SAFETY: Both `redb`'s transaction and table types are `Send + Sync`. +unsafe impl<T: Table + 'static> DatabaseRo<T> for RedbTableRw<'_, T::Key, T::Value> { + #[inline] + fn get(&self, key: &T::Key) -> Result<T::Value, RuntimeError> { + get::<T>(self, key) + } + + #[inline] + fn len(&self) -> Result<u64, RuntimeError> { + len::<T>(self) + } + + #[inline] + fn first(&self) -> Result<(T::Key, T::Value), RuntimeError> { + first::<T>(self) + } + + #[inline] + fn last(&self) -> Result<(T::Key, T::Value), RuntimeError> { + last::<T>(self) + } + + #[inline] + fn is_empty(&self) -> Result<bool, RuntimeError> { + is_empty::<T>(self) + } +} + +impl<T: Table + 'static> DatabaseRw<T> for RedbTableRw<'_, T::Key, T::Value> { + // `redb` returns the value after function calls so we end with Ok(()) instead. + + #[inline] + fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> { + redb::Table::insert(self, key, value)?; + Ok(()) + } + + #[inline] + fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> { + redb::Table::remove(self, key)?; + Ok(()) + } + + #[inline] + fn take(&mut self, key: &T::Key) -> Result<T::Value, RuntimeError> { + if let Some(value) = redb::Table::remove(self, key)? { + Ok(value.value()) + } else { + Err(RuntimeError::KeyNotFound) + } + } + + #[inline] + fn pop_first(&mut self) -> Result<(T::Key, T::Value), RuntimeError> { + let (key, value) = redb::Table::pop_first(self)?.ok_or(RuntimeError::KeyNotFound)?; + Ok((key.value(), value.value())) + } + + #[inline] + fn pop_last(&mut self) -> Result<(T::Key, T::Value), RuntimeError> { + let (key, value) = redb::Table::pop_last(self)?.ok_or(RuntimeError::KeyNotFound)?; + Ok((key.value(), value.value())) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/redb/env.rs b/storage/database/src/backend/redb/env.rs new file mode 100644 index 00000000..e552d454 --- /dev/null +++ b/storage/database/src/backend/redb/env.rs @@ -0,0 +1,226 @@ +//! Implementation of `trait Env` for `redb`. + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + backend::redb::storable::StorableRedb, + config::{Config, SyncMode}, + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + env::{Env, EnvInner}, + error::{InitError, RuntimeError}, + table::Table, + tables::call_fn_on_all_tables_or_early_return, + TxRw, +}; + +//---------------------------------------------------------------------------------------------------- ConcreteEnv +/// A strongly typed, concrete database environment, backed by `redb`. +pub struct ConcreteEnv { + /// The actual database environment. + env: redb::Database, + + /// The configuration we were opened with + /// (and in current use). + config: Config, + + /// A cached, redb version of `cuprate_database::config::SyncMode`. + /// `redb` needs the sync mode to be set _per_ TX, so we + /// will continue to use this value every `Env::tx_rw`. + durability: redb::Durability, +} + +impl Drop for ConcreteEnv { + fn drop(&mut self) { + // INVARIANT: drop(ConcreteEnv) must sync. + if let Err(e) = self.sync() { + // TODO: use tracing + println!("{e:#?}"); + } + + // TODO: log that we are dropping the database. + } +} + +//---------------------------------------------------------------------------------------------------- Env Impl +impl Env for ConcreteEnv { + const MANUAL_RESIZE: bool = false; + const SYNCS_PER_TX: bool = false; + type EnvInner<'env> = (&'env redb::Database, redb::Durability); + type TxRo<'tx> = redb::ReadTransaction; + type TxRw<'tx> = redb::WriteTransaction; + + #[cold] + #[inline(never)] // called once. + fn open(config: Config) -> Result<Self, InitError> { + // SOMEDAY: dynamic syncs are not implemented. + let durability = match config.sync_mode { + // FIXME: There's also `redb::Durability::Paranoid`: + // <https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Paranoid> + // should we use that instead of Immediate? + SyncMode::Safe => redb::Durability::Immediate, + SyncMode::Async => redb::Durability::Eventual, + SyncMode::Fast => redb::Durability::None, + // SOMEDAY: dynamic syncs are not implemented. + SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(), + }; + + let env_builder = redb::Builder::new(); + + // FIXME: we can set cache sizes with: + // env_builder.set_cache(bytes); + + // Use the in-memory backend if the feature is enabled. + let mut env = if cfg!(feature = "redb-memory") { + env_builder.create_with_backend(redb::backends::InMemoryBackend::new())? + } else { + // Create the database directory if it doesn't exist. + std::fs::create_dir_all(config.db_directory())?; + + // Open the database file, create if needed. + let db_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(config.db_file())?; + + env_builder.create_file(db_file)? + }; + + // Create all database tables. + // `redb` creates tables if they don't exist. + // <https://docs.rs/redb/latest/redb/struct.WriteTransaction.html#method.open_table> + + /// Function that creates the tables based off the passed `T: Table`. + fn create_table<T: Table>(tx_rw: &redb::WriteTransaction) -> Result<(), InitError> { + let table: redb::TableDefinition< + 'static, + StorableRedb<<T as Table>::Key>, + StorableRedb<<T as Table>::Value>, + > = redb::TableDefinition::new(<T as Table>::NAME); + + // `redb` creates tables on open if not already created. + tx_rw.open_table(table)?; + Ok(()) + } + + // Create all tables. + // FIXME: this macro is kinda awkward. + let mut tx_rw = env.begin_write()?; + { + let tx_rw = &mut tx_rw; + match call_fn_on_all_tables_or_early_return!(create_table(tx_rw)) { + Ok(_) => (), + Err(e) => return Err(e), + } + } + tx_rw.commit()?; + + // Check for file integrity. + // FIXME: should we do this? is it slow? + env.check_integrity()?; + + Ok(Self { + env, + config, + durability, + }) + } + + fn config(&self) -> &Config { + &self.config + } + + fn sync(&self) -> Result<(), RuntimeError> { + // `redb`'s syncs are tied with write transactions, + // so just create one, don't do anything and commit. + let mut tx_rw = self.env.begin_write()?; + tx_rw.set_durability(redb::Durability::Paranoid); + TxRw::commit(tx_rw) + } + + fn env_inner(&self) -> Self::EnvInner<'_> { + (&self.env, self.durability) + } +} + +//---------------------------------------------------------------------------------------------------- EnvInner Impl +impl<'env> EnvInner<'env, redb::ReadTransaction, redb::WriteTransaction> + for (&'env redb::Database, redb::Durability) +where + Self: 'env, +{ + #[inline] + fn tx_ro(&'env self) -> Result<redb::ReadTransaction, RuntimeError> { + Ok(self.0.begin_read()?) + } + + #[inline] + fn tx_rw(&'env self) -> Result<redb::WriteTransaction, RuntimeError> { + // `redb` has sync modes on the TX level, unlike heed, + // which sets it at the Environment level. + // + // So, set the durability here before returning the TX. + let mut tx_rw = self.0.begin_write()?; + tx_rw.set_durability(self.1); + Ok(tx_rw) + } + + #[inline] + fn open_db_ro<T: Table>( + &self, + tx_ro: &redb::ReadTransaction, + ) -> Result<impl DatabaseRo<T> + DatabaseIter<T>, RuntimeError> { + // Open up a read-only database using our `T: Table`'s const metadata. + let table: redb::TableDefinition<'static, StorableRedb<T::Key>, StorableRedb<T::Value>> = + redb::TableDefinition::new(T::NAME); + + // INVARIANT: Our `?` error conversion will panic if the table does not exist. + Ok(tx_ro.open_table(table)?) + } + + #[inline] + fn open_db_rw<T: Table>( + &self, + tx_rw: &redb::WriteTransaction, + ) -> Result<impl DatabaseRw<T>, RuntimeError> { + // Open up a read/write database using our `T: Table`'s const metadata. + let table: redb::TableDefinition<'static, StorableRedb<T::Key>, StorableRedb<T::Value>> = + redb::TableDefinition::new(T::NAME); + + // `redb` creates tables if they don't exist, so this should never panic. + // <https://docs.rs/redb/latest/redb/struct.WriteTransaction.html#method.open_table> + Ok(tx_rw.open_table(table)?) + } + + #[inline] + fn clear_db<T: Table>(&self, tx_rw: &mut redb::WriteTransaction) -> Result<(), RuntimeError> { + let table: redb::TableDefinition< + 'static, + StorableRedb<<T as Table>::Key>, + StorableRedb<<T as Table>::Value>, + > = redb::TableDefinition::new(<T as Table>::NAME); + + // INVARIANT: + // This `delete_table()` will not run into this `TableAlreadyOpen` error: + // <https://docs.rs/redb/2.0.0/src/redb/transactions.rs.html#382> + // which will panic in the `From` impl, as: + // + // 1. Only 1 `redb::WriteTransaction` can exist at a time + // 2. We have exclusive access to it + // 3. So it's not being used to open a table since that needs `&tx_rw` + // + // Reader-open tables do not affect this, if they're open the below is still OK. + redb::WriteTransaction::delete_table(tx_rw, table)?; + // Re-create the table. + // `redb` creates tables if they don't exist, so this should never panic. + redb::WriteTransaction::open_table(tx_rw, table)?; + + Ok(()) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/redb/error.rs b/storage/database/src/backend/redb/error.rs new file mode 100644 index 00000000..4d40dbd9 --- /dev/null +++ b/storage/database/src/backend/redb/error.rs @@ -0,0 +1,172 @@ +//! Conversion from `redb`'s errors -> `cuprate_database`'s errors. +//! +//! HACK: There's a lot of `_ =>` usage here because +//! `redb`'s errors are `#[non_exhaustive]`... + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + constants::DATABASE_CORRUPT_MSG, + error::{InitError, RuntimeError}, +}; + +//---------------------------------------------------------------------------------------------------- InitError +impl From<redb::DatabaseError> for InitError { + /// Created by `redb` in: + /// - [`redb::Database::open`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.open). + fn from(error: redb::DatabaseError) -> Self { + use redb::DatabaseError as E; + use redb::StorageError as E2; + + // Reference of all possible errors `redb` will return + // upon using `redb::Database::open`: + // <https://docs.rs/redb/1.5.0/src/redb/db.rs.html#908-923> + match error { + E::RepairAborted => Self::Corrupt, + E::UpgradeRequired(_) => Self::InvalidVersion, + E::Storage(s_error) => match s_error { + E2::Io(e) => Self::Io(e), + E2::Corrupted(_) => Self::Corrupt, + + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(s_error)), + }, + + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(error)), + } + } +} + +impl From<redb::StorageError> for InitError { + /// Created by `redb` in: + /// - [`redb::Database::open`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.check_integrity) + fn from(error: redb::StorageError) -> Self { + use redb::StorageError as E; + + match error { + E::Io(e) => Self::Io(e), + E::Corrupted(_) => Self::Corrupt, + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(error)), + } + } +} + +impl From<redb::TransactionError> for InitError { + /// Created by `redb` in: + /// - [`redb::Database::begin_write`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_write) + fn from(error: redb::TransactionError) -> Self { + match error { + redb::TransactionError::Storage(error) => error.into(), + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(error)), + } + } +} + +impl From<redb::TableError> for InitError { + /// Created by `redb` in: + /// - [`redb::WriteTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.open_table) + fn from(error: redb::TableError) -> Self { + use redb::TableError as E; + + match error { + E::Storage(error) => error.into(), + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(error)), + } + } +} + +impl From<redb::CommitError> for InitError { + /// Created by `redb` in: + /// - [`redb::WriteTransaction::commit`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.commit) + fn from(error: redb::CommitError) -> Self { + match error { + redb::CommitError::Storage(error) => error.into(), + // HACK: Handle new errors as `redb` adds them. + _ => Self::Unknown(Box::new(error)), + } + } +} + +//---------------------------------------------------------------------------------------------------- RuntimeError +#[allow(clippy::fallible_impl_from)] // We need to panic sometimes. +impl From<redb::TransactionError> for RuntimeError { + /// Created by `redb` in: + /// - [`redb::Database::begin_write`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_write) + /// - [`redb::Database::begin_read`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_read) + fn from(error: redb::TransactionError) -> Self { + match error { + redb::TransactionError::Storage(error) => error.into(), + + // HACK: Handle new errors as `redb` adds them. + _ => unreachable!(), + } + } +} + +#[allow(clippy::fallible_impl_from)] // We need to panic sometimes. +impl From<redb::CommitError> for RuntimeError { + /// Created by `redb` in: + /// - [`redb::WriteTransaction::commit`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.commit) + fn from(error: redb::CommitError) -> Self { + match error { + redb::CommitError::Storage(error) => error.into(), + + // HACK: Handle new errors as `redb` adds them. + _ => unreachable!(), + } + } +} + +#[allow(clippy::fallible_impl_from)] // We need to panic sometimes. +impl From<redb::TableError> for RuntimeError { + /// Created by `redb` in: + /// - [`redb::WriteTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.open_table) + /// - [`redb::ReadTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.ReadTransaction.html#method.open_table) + fn from(error: redb::TableError) -> Self { + use redb::TableError as E; + + match error { + E::Storage(error) => error.into(), + + // Only if we write incorrect code. + E::TableTypeMismatch { .. } + | E::TableIsMultimap(_) + | E::TableIsNotMultimap(_) + | E::TypeDefinitionChanged { .. } + | E::TableDoesNotExist(_) + | E::TableAlreadyOpen(..) => panic!("fix the database code! {error:#?}"), + + // HACK: Handle new errors as `redb` adds them. + _ => unreachable!(), + } + } +} + +#[allow(clippy::fallible_impl_from)] // We need to panic sometimes. +impl From<redb::StorageError> for RuntimeError { + /// Created by `redb` in: + /// - [`redb::Table`](https://docs.rs/redb/1.5.0/redb/struct.Table.html) functions + /// - [`redb::ReadOnlyTable`](https://docs.rs/redb/1.5.0/redb/struct.ReadOnlyTable.html) functions + fn from(error: redb::StorageError) -> Self { + use redb::StorageError as E; + + match error { + E::Io(e) => Self::Io(e), + E::Corrupted(s) => panic!("{s:#?}\n{DATABASE_CORRUPT_MSG}"), + E::ValueTooLarge(s) => panic!("fix the database code! {s:#?}"), + E::LockPoisoned(s) => panic!("{s:#?}"), + + // HACK: Handle new errors as `redb` adds them. + _ => unreachable!(), + } + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/redb/mod.rs b/storage/database/src/backend/redb/mod.rs new file mode 100644 index 00000000..0049143d --- /dev/null +++ b/storage/database/src/backend/redb/mod.rs @@ -0,0 +1,9 @@ +//! Database backend implementation backed by `sanakirja`. + +mod env; +pub use env::ConcreteEnv; +mod database; +mod error; +mod storable; +mod transaction; +mod types; diff --git a/storage/database/src/backend/redb/storable.rs b/storage/database/src/backend/redb/storable.rs new file mode 100644 index 00000000..6735fec0 --- /dev/null +++ b/storage/database/src/backend/redb/storable.rs @@ -0,0 +1,221 @@ +//! `cuprate_database::Storable` <-> `redb` serde trait compatibility layer. + +//---------------------------------------------------------------------------------------------------- Use +use std::{cmp::Ordering, fmt::Debug, marker::PhantomData}; + +use redb::TypeName; + +use crate::{key::Key, storable::Storable}; + +//---------------------------------------------------------------------------------------------------- StorableRedb +/// The glue structs that implements `redb`'s (de)serialization +/// traits on any type that implements `cuprate_database::Key`. +/// +/// Never actually get constructed, just used for trait bound translations. +#[derive(Debug)] +pub(super) struct StorableRedb<T>(PhantomData<T>) +where + T: Storable; + +//---------------------------------------------------------------------------------------------------- redb::Key +// If `Key` is also implemented, this can act as a `redb::Key`. +impl<T> redb::Key for StorableRedb<T> +where + T: Key + 'static, +{ + #[inline] + fn compare(left: &[u8], right: &[u8]) -> Ordering { + <T as Key>::compare(left, right) + } +} + +//---------------------------------------------------------------------------------------------------- redb::Value +impl<T> redb::Value for StorableRedb<T> +where + T: Storable + 'static, +{ + type SelfType<'a> = T where Self: 'a; + type AsBytes<'a> = &'a [u8] where Self: 'a; + + #[inline] + fn fixed_width() -> Option<usize> { + <T as Storable>::BYTE_LENGTH + } + + #[inline] + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'static> + where + Self: 'a, + { + <T as Storable>::from_bytes(data) + } + + #[inline] + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'a + 'b, + { + <T as Storable>::as_bytes(value) + } + + #[inline] + fn type_name() -> TypeName { + TypeName::new(std::any::type_name::<T>()) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +#[allow(clippy::needless_pass_by_value)] +mod test { + use super::*; + use crate::{StorableBytes, StorableVec}; + + // Each `#[test]` function has a `test()` to: + // - log + // - simplify trait bounds + // - make sure the right function is being called + + #[test] + /// Assert `redb::Key::compare` works for `StorableRedb`. + fn compare() { + fn test<T>(left: T, right: T, expected: Ordering) + where + T: Key + 'static, + { + println!("left: {left:?}, right: {right:?}, expected: {expected:?}"); + assert_eq!( + <StorableRedb::<T> as redb::Key>::compare( + <StorableRedb::<T> as redb::Value>::as_bytes(&left), + <StorableRedb::<T> as redb::Value>::as_bytes(&right) + ), + expected + ); + } + + test::<i64>(-1, 2, Ordering::Greater); // bytes are greater, not the value + test::<u64>(0, 1, Ordering::Less); + test::<[u8; 2]>([1, 1], [1, 0], Ordering::Greater); + test::<[u8; 3]>([1, 2, 3], [1, 2, 3], Ordering::Equal); + } + + #[test] + /// Assert `redb::Key::fixed_width` is accurate. + fn fixed_width() { + fn test<T>(expected: Option<usize>) + where + T: Storable + 'static, + { + assert_eq!(<StorableRedb::<T> as redb::Value>::fixed_width(), expected); + } + + test::<()>(Some(0)); + test::<u8>(Some(1)); + test::<u16>(Some(2)); + test::<u32>(Some(4)); + test::<u64>(Some(8)); + test::<i8>(Some(1)); + test::<i16>(Some(2)); + test::<i32>(Some(4)); + test::<i64>(Some(8)); + test::<StorableVec<u8>>(None); + test::<StorableBytes>(None); + test::<[u8; 0]>(Some(0)); + test::<[u8; 1]>(Some(1)); + test::<[u8; 2]>(Some(2)); + test::<[u8; 3]>(Some(3)); + } + + #[test] + /// Assert `redb::Key::as_bytes` is accurate. + fn as_bytes() { + fn test<T>(t: &T, expected: &[u8]) + where + T: Storable + 'static, + { + println!("t: {t:?}, expected: {expected:?}"); + assert_eq!(<StorableRedb::<T> as redb::Value>::as_bytes(t), expected); + } + + test::<()>(&(), &[]); + test::<u8>(&0, &[0]); + test::<u16>(&1, &[1, 0]); + test::<u32>(&2, &[2, 0, 0, 0]); + test::<u64>(&3, &[3, 0, 0, 0, 0, 0, 0, 0]); + test::<i8>(&-1, &[255]); + test::<i16>(&-2, &[254, 255]); + test::<i32>(&-3, &[253, 255, 255, 255]); + test::<i64>(&-4, &[252, 255, 255, 255, 255, 255, 255, 255]); + test::<StorableVec<u8>>(&StorableVec([1, 2].to_vec()), &[1, 2]); + test::<StorableBytes>(&StorableBytes(bytes::Bytes::from_static(&[1, 2])), &[1, 2]); + test::<[u8; 0]>(&[], &[]); + test::<[u8; 1]>(&[255], &[255]); + test::<[u8; 2]>(&[111, 0], &[111, 0]); + test::<[u8; 3]>(&[1, 0, 1], &[1, 0, 1]); + } + + #[test] + /// Assert `redb::Key::from_bytes` is accurate. + fn from_bytes() { + fn test<T>(bytes: &[u8], expected: &T) + where + T: Storable + PartialEq + 'static, + { + println!("bytes: {bytes:?}, expected: {expected:?}"); + assert_eq!( + &<StorableRedb::<T> as redb::Value>::from_bytes(bytes), + expected + ); + } + + test::<()>([].as_slice(), &()); + test::<u8>([0].as_slice(), &0); + test::<u16>([1, 0].as_slice(), &1); + test::<u32>([2, 0, 0, 0].as_slice(), &2); + test::<u64>([3, 0, 0, 0, 0, 0, 0, 0].as_slice(), &3); + test::<i8>([255].as_slice(), &-1); + test::<i16>([254, 255].as_slice(), &-2); + test::<i32>([253, 255, 255, 255].as_slice(), &-3); + test::<i64>([252, 255, 255, 255, 255, 255, 255, 255].as_slice(), &-4); + test::<StorableVec<u8>>(&[1, 2], &StorableVec(vec![1, 2])); + test::<StorableBytes>(&[1, 2], &StorableBytes(bytes::Bytes::from_static(&[1, 2]))); + test::<[u8; 0]>([].as_slice(), &[]); + test::<[u8; 1]>([255].as_slice(), &[255]); + test::<[u8; 2]>([111, 0].as_slice(), &[111, 0]); + test::<[u8; 3]>([1, 0, 1].as_slice(), &[1, 0, 1]); + } + + #[test] + /// Assert `redb::Key::type_name` returns unique names. + /// The name itself isn't tested, the invariant is that + /// they are all unique. + fn type_name() { + // Can't use a proper set because `redb::TypeName: !Ord`. + let set = [ + <StorableRedb<()> as redb::Value>::type_name(), + <StorableRedb<u8> as redb::Value>::type_name(), + <StorableRedb<u16> as redb::Value>::type_name(), + <StorableRedb<u32> as redb::Value>::type_name(), + <StorableRedb<u64> as redb::Value>::type_name(), + <StorableRedb<i8> as redb::Value>::type_name(), + <StorableRedb<i16> as redb::Value>::type_name(), + <StorableRedb<i32> as redb::Value>::type_name(), + <StorableRedb<i64> as redb::Value>::type_name(), + <StorableRedb<StorableVec<u8>> as redb::Value>::type_name(), + <StorableRedb<StorableBytes> as redb::Value>::type_name(), + <StorableRedb<[u8; 0]> as redb::Value>::type_name(), + <StorableRedb<[u8; 1]> as redb::Value>::type_name(), + <StorableRedb<[u8; 2]> as redb::Value>::type_name(), + <StorableRedb<[u8; 3]> as redb::Value>::type_name(), + ]; + + // Check every permutation is unique. + for (index, i) in set.iter().enumerate() { + for (index2, j) in set.iter().enumerate() { + if index != index2 { + assert_ne!(i, j); + } + } + } + } +} diff --git a/storage/database/src/backend/redb/transaction.rs b/storage/database/src/backend/redb/transaction.rs new file mode 100644 index 00000000..5048851d --- /dev/null +++ b/storage/database/src/backend/redb/transaction.rs @@ -0,0 +1,38 @@ +//! Implementation of `trait TxRo/TxRw` for `redb`. + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + error::RuntimeError, + transaction::{TxRo, TxRw}, +}; + +//---------------------------------------------------------------------------------------------------- TxRo +impl TxRo<'_> for redb::ReadTransaction { + /// This function is infallible. + fn commit(self) -> Result<(), RuntimeError> { + // `redb`'s read transactions cleanup automatically when all references are dropped. + // + // There is `close()`: + // <https://docs.rs/redb/2.0.0/redb/struct.ReadTransaction.html#method.close> + // but this will error if there are outstanding references, i.e. an open table. + // This is unwanted behavior in our case, so we don't call this. + Ok(()) + } +} + +//---------------------------------------------------------------------------------------------------- TxRw +impl TxRw<'_> for redb::WriteTransaction { + fn commit(self) -> Result<(), RuntimeError> { + Ok(self.commit()?) + } + + fn abort(self) -> Result<(), RuntimeError> { + Ok(self.abort()?) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/backend/redb/types.rs b/storage/database/src/backend/redb/types.rs new file mode 100644 index 00000000..f3534c55 --- /dev/null +++ b/storage/database/src/backend/redb/types.rs @@ -0,0 +1,11 @@ +//! `redb` type aliases. + +//---------------------------------------------------------------------------------------------------- Types +use crate::backend::redb::storable::StorableRedb; + +//---------------------------------------------------------------------------------------------------- Types +/// The concrete type for readable `redb` tables. +pub(super) type RedbTableRo<K, V> = redb::ReadOnlyTable<StorableRedb<K>, StorableRedb<V>>; + +/// The concrete type for readable/writable `redb` tables. +pub(super) type RedbTableRw<'tx, K, V> = redb::Table<'tx, StorableRedb<K>, StorableRedb<V>>; diff --git a/storage/database/src/backend/tests.rs b/storage/database/src/backend/tests.rs new file mode 100644 index 00000000..03d06c69 --- /dev/null +++ b/storage/database/src/backend/tests.rs @@ -0,0 +1,550 @@ +//! Tests for `cuprate_database`'s backends. +//! +//! These tests are fully trait-based, meaning there +//! is no reference to `backend/`-specific types. +//! +//! As such, which backend is tested is +//! dependant on the feature flags used. +//! +//! | Feature flag | Tested backend | +//! |---------------|----------------| +//! | Only `redb` | `redb` +//! | Anything else | `heed` +//! +//! `redb`, and it only must be enabled for it to be tested. + +//---------------------------------------------------------------------------------------------------- Import + +use crate::{ + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + env::{Env, EnvInner}, + error::RuntimeError, + resize::ResizeAlgorithm, + storable::StorableVec, + tables::{ + BlockBlobs, BlockHeights, BlockInfos, KeyImages, NumOutputs, Outputs, PrunableHashes, + PrunableTxBlobs, PrunedTxBlobs, RctOutputs, TxBlobs, TxHeights, TxIds, TxOutputs, + TxUnlockTime, + }, + tables::{TablesIter, TablesMut}, + tests::tmp_concrete_env, + transaction::{TxRo, TxRw}, + types::{ + Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, + Output, OutputFlags, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, + TxBlob, TxHash, TxId, UnlockTime, + }, + ConcreteEnv, +}; + +//---------------------------------------------------------------------------------------------------- Tests +/// Simply call [`Env::open`]. If this fails, something is really wrong. +#[test] +fn open() { + tmp_concrete_env(); +} + +/// Create database transactions, but don't write any data. +#[test] +fn tx() { + let (env, _tempdir) = tmp_concrete_env(); + let env_inner = env.env_inner(); + + TxRo::commit(env_inner.tx_ro().unwrap()).unwrap(); + TxRw::commit(env_inner.tx_rw().unwrap()).unwrap(); + TxRw::abort(env_inner.tx_rw().unwrap()).unwrap(); +} + +/// Open (and verify) that all database tables +/// exist already after calling [`Env::open`]. +#[test] +fn open_db() { + let (env, _tempdir) = tmp_concrete_env(); + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro().unwrap(); + let tx_rw = env_inner.tx_rw().unwrap(); + + // Open all tables in read-only mode. + // This should be updated when tables are modified. + env_inner.open_db_ro::<BlockBlobs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<BlockHeights>(&tx_ro).unwrap(); + env_inner.open_db_ro::<BlockInfos>(&tx_ro).unwrap(); + env_inner.open_db_ro::<KeyImages>(&tx_ro).unwrap(); + env_inner.open_db_ro::<NumOutputs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<Outputs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<PrunableHashes>(&tx_ro).unwrap(); + env_inner.open_db_ro::<PrunableTxBlobs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<PrunedTxBlobs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<RctOutputs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<TxBlobs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<TxHeights>(&tx_ro).unwrap(); + env_inner.open_db_ro::<TxIds>(&tx_ro).unwrap(); + env_inner.open_db_ro::<TxOutputs>(&tx_ro).unwrap(); + env_inner.open_db_ro::<TxUnlockTime>(&tx_ro).unwrap(); + TxRo::commit(tx_ro).unwrap(); + + // Open all tables in read/write mode. + env_inner.open_db_rw::<BlockBlobs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<BlockHeights>(&tx_rw).unwrap(); + env_inner.open_db_rw::<BlockInfos>(&tx_rw).unwrap(); + env_inner.open_db_rw::<KeyImages>(&tx_rw).unwrap(); + env_inner.open_db_rw::<NumOutputs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<Outputs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<PrunableHashes>(&tx_rw).unwrap(); + env_inner.open_db_rw::<PrunableTxBlobs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<PrunedTxBlobs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<RctOutputs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<TxBlobs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<TxHeights>(&tx_rw).unwrap(); + env_inner.open_db_rw::<TxIds>(&tx_rw).unwrap(); + env_inner.open_db_rw::<TxOutputs>(&tx_rw).unwrap(); + env_inner.open_db_rw::<TxUnlockTime>(&tx_rw).unwrap(); + TxRw::commit(tx_rw).unwrap(); +} + +/// Test `Env` resizes. +#[test] +fn resize() { + // This test is only valid for `Env`'s that need to resize manually. + if !ConcreteEnv::MANUAL_RESIZE { + return; + } + + let (env, _tempdir) = tmp_concrete_env(); + + // Resize by the OS page size. + let page_size = crate::resize::page_size(); + let old_size = env.current_map_size(); + env.resize_map(Some(ResizeAlgorithm::FixedBytes(page_size))); + + // Assert it resized exactly by the OS page size. + let new_size = env.current_map_size(); + assert_eq!(new_size, old_size + page_size.get()); +} + +/// Test that `Env`'s that don't manually resize. +#[test] +#[should_panic = "unreachable"] +fn non_manual_resize_1() { + if ConcreteEnv::MANUAL_RESIZE { + unreachable!(); + } else { + let (env, _tempdir) = tmp_concrete_env(); + env.resize_map(None); + } +} + +#[test] +#[should_panic = "unreachable"] +fn non_manual_resize_2() { + if ConcreteEnv::MANUAL_RESIZE { + unreachable!(); + } else { + let (env, _tempdir) = tmp_concrete_env(); + env.current_map_size(); + } +} + +/// Test all `DatabaseR{o,w}` operations. +#[test] +fn db_read_write() { + let (env, _tempdir) = tmp_concrete_env(); + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw().unwrap(); + let mut table = env_inner.open_db_rw::<Outputs>(&tx_rw).unwrap(); + + /// The (1st) key. + const KEY: PreRctOutputId = PreRctOutputId { + amount: 1, + amount_index: 123, + }; + /// The expected value. + const VALUE: Output = Output { + key: [35; 32], + height: 45_761_798, + output_flags: OutputFlags::empty(), + tx_idx: 2_353_487, + }; + /// How many `(key, value)` pairs will be inserted. + const N: u64 = 100; + + /// Assert 2 `Output`'s are equal, and that accessing + /// their fields don't result in an unaligned panic. + fn assert_same(output: Output) { + assert_eq!(output, VALUE); + assert_eq!(output.key, VALUE.key); + assert_eq!(output.height, VALUE.height); + assert_eq!(output.output_flags, VALUE.output_flags); + assert_eq!(output.tx_idx, VALUE.tx_idx); + } + + assert!(table.is_empty().unwrap()); + + // Insert keys. + let mut key = KEY; + for _ in 0..N { + table.put(&key, &VALUE).unwrap(); + key.amount += 1; + } + + assert_eq!(table.len().unwrap(), N); + + // Assert the first/last `(key, value)`s are there. + { + assert!(table.contains(&KEY).unwrap()); + let get: Output = table.get(&KEY).unwrap(); + assert_same(get); + + let first: Output = table.first().unwrap().1; + assert_same(first); + + let last: Output = table.last().unwrap().1; + assert_same(last); + } + + // Commit transactions, create new ones. + drop(table); + TxRw::commit(tx_rw).unwrap(); + let tx_ro = env_inner.tx_ro().unwrap(); + let table_ro = env_inner.open_db_ro::<Outputs>(&tx_ro).unwrap(); + let tx_rw = env_inner.tx_rw().unwrap(); + let mut table = env_inner.open_db_rw::<Outputs>(&tx_rw).unwrap(); + + // Assert the whole range is there. + { + let range = table_ro.get_range(..).unwrap(); + let mut i = 0; + for result in range { + let value: Output = result.unwrap(); + assert_same(value); + + i += 1; + } + assert_eq!(i, N); + } + + // `get_range()` tests. + let mut key = KEY; + key.amount += N; + let range = KEY..key; + + // Assert count is correct. + assert_eq!( + N as usize, + table_ro.get_range(range.clone()).unwrap().count() + ); + + // Assert each returned value from the iterator is owned. + { + let mut iter = table_ro.get_range(range.clone()).unwrap(); + let value: Output = iter.next().unwrap().unwrap(); // 1. take value out + drop(iter); // 2. drop the `impl Iterator + 'a` + assert_same(value); // 3. assert even without the iterator, the value is alive + } + + // Assert each value is the same. + { + let mut iter = table_ro.get_range(range).unwrap(); + for _ in 0..N { + let value: Output = iter.next().unwrap().unwrap(); + assert_same(value); + } + } + + // Assert `update()` works. + { + const HEIGHT: u32 = 999; + + assert_ne!(table.get(&KEY).unwrap().height, HEIGHT); + + table + .update(&KEY, |mut value| { + value.height = HEIGHT; + Some(value) + }) + .unwrap(); + + assert_eq!(table.get(&KEY).unwrap().height, HEIGHT); + } + + // Assert deleting works. + { + table.delete(&KEY).unwrap(); + let value = table.get(&KEY); + assert!(!table.contains(&KEY).unwrap()); + assert!(matches!(value, Err(RuntimeError::KeyNotFound))); + // Assert the other `(key, value)` pairs are still there. + let mut key = KEY; + key.amount += N - 1; // we used inclusive `0..N` + let value = table.get(&key).unwrap(); + assert_same(value); + } + + // Assert `take()` works. + { + let mut key = KEY; + key.amount += 1; + let value = table.take(&key).unwrap(); + assert_eq!(value, VALUE); + + let get = table.get(&KEY); + assert!(!table.contains(&key).unwrap()); + assert!(matches!(get, Err(RuntimeError::KeyNotFound))); + + // Assert the other `(key, value)` pairs are still there. + key.amount += 1; + let value = table.get(&key).unwrap(); + assert_same(value); + } + + drop(table); + TxRw::commit(tx_rw).unwrap(); + + // Assert `clear_db()` works. + { + let mut tx_rw = env_inner.tx_rw().unwrap(); + env_inner.clear_db::<Outputs>(&mut tx_rw).unwrap(); + let table = env_inner.open_db_rw::<Outputs>(&tx_rw).unwrap(); + assert!(table.is_empty().unwrap()); + for n in 0..N { + let mut key = KEY; + key.amount += n; + let value = table.get(&key); + assert!(matches!(value, Err(RuntimeError::KeyNotFound))); + assert!(!table.contains(&key).unwrap()); + } + + // Reader still sees old value. + assert!(!table_ro.is_empty().unwrap()); + + // Writer sees updated value (nothing). + assert!(table.is_empty().unwrap()); + } +} + +/// Assert that `key`'s in database tables are sorted in +/// an ordered B-Tree fashion, i.e. `min_value -> max_value`. +#[test] +fn tables_are_sorted() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables_mut = env_inner.open_tables_mut(&tx_rw).unwrap(); + + // Insert `{5, 4, 3, 2, 1, 0}`, assert each new + // number inserted is the minimum `first()` value. + for key in (0..6).rev() { + tables_mut.num_outputs_mut().put(&key, &123).unwrap(); + let (first, _) = tables_mut.num_outputs_mut().first().unwrap(); + assert_eq!(first, key); + } + + drop(tables_mut); + TxRw::commit(tx_rw).unwrap(); + let tx_rw = env_inner.tx_rw().unwrap(); + + // Assert iterators are ordered. + { + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + let t = tables.num_outputs_iter(); + let iter = t.iter().unwrap(); + let keys = t.keys().unwrap(); + for ((i, iter), key) in (0..6).zip(iter).zip(keys) { + let (iter, _) = iter.unwrap(); + let key = key.unwrap(); + assert_eq!(i, iter); + assert_eq!(iter, key); + } + } + + let mut tables_mut = env_inner.open_tables_mut(&tx_rw).unwrap(); + let t = tables_mut.num_outputs_mut(); + + // Assert the `first()` values are the minimum, i.e. `{0, 1, 2}` + for key in 0..3 { + let (first, _) = t.first().unwrap(); + assert_eq!(first, key); + t.delete(&key).unwrap(); + } + + // Assert the `last()` values are the maximum, i.e. `{5, 4, 3}` + for key in (3..6).rev() { + let (last, _) = tables_mut.num_outputs_mut().last().unwrap(); + assert_eq!(last, key); + tables_mut.num_outputs_mut().delete(&key).unwrap(); + } +} + +//---------------------------------------------------------------------------------------------------- Table Tests +/// Test multiple tables and their key + values. +/// +/// Each one of these tests: +/// - Opens a specific table +/// - Essentially does the `db_read_write` test +macro_rules! test_tables { + ($( + $table:ident, // Table type + $key_type:ty => // Key (type) + $value_type:ty, // Value (type) + $key:expr => // Key (the value) + $value:expr, // Value (the value) + )* $(,)?) => { paste::paste! { $( + // Test function's name is the table type in `snake_case`. + #[test] + fn [<$table:snake>]() { + // Open the database env and table. + let (env, _tempdir) = tmp_concrete_env(); + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw().unwrap(); + let mut table = env_inner.open_db_rw::<$table>(&mut tx_rw).unwrap(); + + /// The expected key. + const KEY: $key_type = $key; + // The expected value. + let value: $value_type = $value; + + // Assert a passed value is equal to the const value. + let assert_eq = |v: &$value_type| { + assert_eq!(v, &value); + }; + + // Insert the key. + table.put(&KEY, &value).unwrap(); + // Assert key is there. + { + let value: $value_type = table.get(&KEY).unwrap(); + assert_eq(&value); + } + + assert!(table.contains(&KEY).unwrap()); + assert_eq!(table.len().unwrap(), 1); + + // Commit transactions, create new ones. + drop(table); + TxRw::commit(tx_rw).unwrap(); + let mut tx_rw = env_inner.tx_rw().unwrap(); + let tx_ro = env_inner.tx_ro().unwrap(); + let mut table = env_inner.open_db_rw::<$table>(&tx_rw).unwrap(); + let table_ro = env_inner.open_db_ro::<$table>(&tx_ro).unwrap(); + + // Assert `get_range()` works. + { + let range = KEY..; + assert_eq!(1, table_ro.get_range(range.clone()).unwrap().count()); + let mut iter = table_ro.get_range(range).unwrap(); + let value = iter.next().unwrap().unwrap(); + assert_eq(&value); + } + + // Assert deleting works. + { + table.delete(&KEY).unwrap(); + let value = table.get(&KEY); + assert!(matches!(value, Err(RuntimeError::KeyNotFound))); + assert!(!table.contains(&KEY).unwrap()); + assert_eq!(table.len().unwrap(), 0); + } + + table.put(&KEY, &value).unwrap(); + + // Assert `clear_db()` works. + { + drop(table); + env_inner.clear_db::<$table>(&mut tx_rw).unwrap(); + let table = env_inner.open_db_rw::<$table>(&mut tx_rw).unwrap(); + let value = table.get(&KEY); + assert!(matches!(value, Err(RuntimeError::KeyNotFound))); + assert!(!table.contains(&KEY).unwrap()); + assert_eq!(table.len().unwrap(), 0); + } + } + )*}}; +} + +// Notes: +// - Keep this sorted A-Z (by table name) +test_tables! { + BlockBlobs, // Table type + BlockHeight => BlockBlob, // Key type => Value type + 123 => StorableVec(vec![1,2,3,4,5,6,7,8]), // Actual key => Actual value + + BlockHeights, + BlockHash => BlockHeight, + [32; 32] => 123, + + BlockInfos, + BlockHeight => BlockInfo, + 123 => BlockInfo { + timestamp: 1, + cumulative_generated_coins: 123, + weight: 321, + cumulative_difficulty_low: 111, + cumulative_difficulty_high: 111, + block_hash: [54; 32], + cumulative_rct_outs: 2389, + long_term_weight: 2389, + }, + + KeyImages, + KeyImage => (), + [32; 32] => (), + + NumOutputs, + Amount => AmountIndex, + 123 => 123, + + TxBlobs, + TxId => TxBlob, + 123 => StorableVec(vec![1,2,3,4,5,6,7,8]), + + TxIds, + TxHash => TxId, + [32; 32] => 123, + + TxHeights, + TxId => BlockHeight, + 123 => 123, + + TxOutputs, + TxId => AmountIndices, + 123 => StorableVec(vec![1,2,3,4,5,6,7,8]), + + TxUnlockTime, + TxId => UnlockTime, + 123 => 123, + + Outputs, + PreRctOutputId => Output, + PreRctOutputId { + amount: 1, + amount_index: 2, + } => Output { + key: [1; 32], + height: 1, + output_flags: OutputFlags::empty(), + tx_idx: 3, + }, + + PrunedTxBlobs, + TxId => PrunedBlob, + 123 => StorableVec(vec![1,2,3,4,5,6,7,8]), + + PrunableTxBlobs, + TxId => PrunableBlob, + 123 => StorableVec(vec![1,2,3,4,5,6,7,8]), + + PrunableHashes, + TxId => PrunableHash, + 123 => [32; 32], + + RctOutputs, + AmountIndex => RctOutput, + 123 => RctOutput { + key: [1; 32], + height: 1, + output_flags: OutputFlags::empty(), + tx_idx: 3, + commitment: [3; 32], + }, +} diff --git a/storage/database/src/config/backend.rs b/storage/database/src/config/backend.rs new file mode 100644 index 00000000..4bbb12ca --- /dev/null +++ b/storage/database/src/config/backend.rs @@ -0,0 +1,31 @@ +//! SOMEDAY + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + borrow::Cow, + num::NonZeroUsize, + path::{Path, PathBuf}, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use cuprate_helper::fs::cuprate_database_dir; + +use crate::{ + config::{ReaderThreads, SyncMode}, + constants::DATABASE_DATA_FILENAME, + resize::ResizeAlgorithm, +}; + +//---------------------------------------------------------------------------------------------------- Backend +/// SOMEDAY: allow runtime hot-swappable backends. +#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Backend { + #[default] + /// SOMEDAY + Heed, + /// SOMEDAY + Redb, +} diff --git a/storage/database/src/config/config.rs b/storage/database/src/config/config.rs new file mode 100644 index 00000000..d712cb69 --- /dev/null +++ b/storage/database/src/config/config.rs @@ -0,0 +1,237 @@ +//! The main [`Config`] struct, holding all configurable values. + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use cuprate_helper::fs::cuprate_database_dir; + +use crate::{ + config::{ReaderThreads, SyncMode}, + constants::DATABASE_DATA_FILENAME, + resize::ResizeAlgorithm, +}; + +//---------------------------------------------------------------------------------------------------- ConfigBuilder +/// Builder for [`Config`]. +/// +// SOMEDAY: there's are many more options to add in the future. +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ConfigBuilder { + /// [`Config::db_directory`]. + db_directory: Option<Cow<'static, Path>>, + + /// [`Config::sync_mode`]. + sync_mode: Option<SyncMode>, + + /// [`Config::reader_threads`]. + reader_threads: Option<ReaderThreads>, + + /// [`Config::resize_algorithm`]. + resize_algorithm: Option<ResizeAlgorithm>, +} + +impl ConfigBuilder { + /// Create a new [`ConfigBuilder`]. + /// + /// [`ConfigBuilder::build`] can be called immediately + /// after this function to use default values. + pub const fn new() -> Self { + Self { + db_directory: None, + sync_mode: None, + reader_threads: None, + resize_algorithm: None, + } + } + + /// Build into a [`Config`]. + /// + /// # Default values + /// If [`ConfigBuilder::db_directory`] was not called, + /// the default [`cuprate_database_dir`] will be used. + /// + /// For all other values, [`Default::default`] is used. + pub fn build(self) -> Config { + // INVARIANT: all PATH safety checks are done + // in `helper::fs`. No need to do them here. + let db_directory = self + .db_directory + .unwrap_or_else(|| Cow::Borrowed(cuprate_database_dir())); + + // Add the database filename to the directory. + let db_file = { + let mut db_file = db_directory.to_path_buf(); + db_file.push(DATABASE_DATA_FILENAME); + Cow::Owned(db_file) + }; + + Config { + db_directory, + db_file, + sync_mode: self.sync_mode.unwrap_or_default(), + reader_threads: self.reader_threads.unwrap_or_default(), + resize_algorithm: self.resize_algorithm.unwrap_or_default(), + } + } + + /// Set a custom database directory (and file) [`Path`]. + #[must_use] + pub fn db_directory(mut self, db_directory: PathBuf) -> Self { + self.db_directory = Some(Cow::Owned(db_directory)); + self + } + + /// Tune the [`ConfigBuilder`] for the highest performing, + /// but also most resource-intensive & maybe risky settings. + /// + /// Good default for testing, and resource-available machines. + #[must_use] + pub fn fast(mut self) -> Self { + self.sync_mode = Some(SyncMode::Fast); + self.reader_threads = Some(ReaderThreads::OnePerThread); + self.resize_algorithm = Some(ResizeAlgorithm::default()); + self + } + + /// Tune the [`ConfigBuilder`] for the lowest performing, + /// but also least resource-intensive settings. + /// + /// Good default for resource-limited machines, e.g. a cheap VPS. + #[must_use] + pub fn low_power(mut self) -> Self { + self.sync_mode = Some(SyncMode::default()); + self.reader_threads = Some(ReaderThreads::One); + self.resize_algorithm = Some(ResizeAlgorithm::default()); + self + } + + /// Set a custom [`SyncMode`]. + #[must_use] + pub const fn sync_mode(mut self, sync_mode: SyncMode) -> Self { + self.sync_mode = Some(sync_mode); + self + } + + /// Set a custom [`ReaderThreads`]. + #[must_use] + pub const fn reader_threads(mut self, reader_threads: ReaderThreads) -> Self { + self.reader_threads = Some(reader_threads); + self + } + + /// Set a custom [`ResizeAlgorithm`]. + #[must_use] + pub const fn resize_algorithm(mut self, resize_algorithm: ResizeAlgorithm) -> Self { + self.resize_algorithm = Some(resize_algorithm); + self + } +} + +impl Default for ConfigBuilder { + fn default() -> Self { + Self { + db_directory: Some(Cow::Borrowed(cuprate_database_dir())), + sync_mode: Some(SyncMode::default()), + reader_threads: Some(ReaderThreads::default()), + resize_algorithm: Some(ResizeAlgorithm::default()), + } + } +} + +//---------------------------------------------------------------------------------------------------- Config +/// Database [`Env`](crate::Env) configuration. +/// +/// This is the struct passed to [`Env::open`](crate::Env::open) that +/// allows the database to be configured in various ways. +/// +/// For construction, either use [`ConfigBuilder`] or [`Config::default`]. +/// +// SOMEDAY: there's are many more options to add in the future. +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Config { + //------------------------ Database PATHs + // These are private since we don't want + // users messing with them after construction. + /// The directory used to store all database files. + /// + /// By default, if no value is provided in the [`Config`] + /// constructor functions, this will be [`cuprate_database_dir`]. + /// + // SOMEDAY: we should also support `/etc/cuprated.conf`. + // This could be represented with an `enum DbPath { Default, Custom, Etc, }` + pub(crate) db_directory: Cow<'static, Path>, + /// The actual database data file. + /// + /// This is private, and created from the above `db_directory`. + pub(crate) db_file: Cow<'static, Path>, + + /// Disk synchronization mode. + pub sync_mode: SyncMode, + + /// Database reader thread count. + pub reader_threads: ReaderThreads, + + /// Database memory map resizing algorithm. + /// + /// This is used as the default fallback, but + /// custom algorithms can be used as well with + /// [`Env::resize_map`](crate::Env::resize_map). + pub resize_algorithm: ResizeAlgorithm, +} + +impl Config { + /// Create a new [`Config`] with sane default settings. + /// + /// The [`Config::db_directory`] will be [`cuprate_database_dir`]. + /// + /// All other values will be [`Default::default`]. + /// + /// Same as [`Config::default`]. + /// + /// ```rust + /// use cuprate_database::{config::*, resize::*, DATABASE_DATA_FILENAME}; + /// use cuprate_helper::fs::*; + /// + /// let config = Config::new(); + /// + /// assert_eq!(config.db_directory(), cuprate_database_dir()); + /// assert!(config.db_file().starts_with(cuprate_database_dir())); + /// assert!(config.db_file().ends_with(DATABASE_DATA_FILENAME)); + /// assert_eq!(config.sync_mode, SyncMode::default()); + /// assert_eq!(config.reader_threads, ReaderThreads::default()); + /// assert_eq!(config.resize_algorithm, ResizeAlgorithm::default()); + /// ``` + pub fn new() -> Self { + ConfigBuilder::default().build() + } + + /// Return the absolute [`Path`] to the database directory. + pub const fn db_directory(&self) -> &Cow<'_, Path> { + &self.db_directory + } + + /// Return the absolute [`Path`] to the database data file. + pub const fn db_file(&self) -> &Cow<'_, Path> { + &self.db_file + } +} + +impl Default for Config { + /// Same as [`Config::new`]. + /// + /// ```rust + /// # use cuprate_database::config::*; + /// assert_eq!(Config::default(), Config::new()); + /// ``` + fn default() -> Self { + Self::new() + } +} diff --git a/storage/database/src/config/mod.rs b/storage/database/src/config/mod.rs new file mode 100644 index 00000000..dfa4f674 --- /dev/null +++ b/storage/database/src/config/mod.rs @@ -0,0 +1,47 @@ +//! Database [`Env`](crate::Env) configuration. +//! +//! This module contains the main [`Config`]uration struct +//! for the database [`Env`](crate::Env)ironment, and types +//! related to configuration settings. +//! +//! The main constructor is the [`ConfigBuilder`]. +//! +//! These configurations are processed at runtime, meaning +//! the `Env` can/will dynamically adjust its behavior +//! based on these values. +//! +//! # Example +//! ```rust +//! use cuprate_database::{ +//! Env, +//! config::{ConfigBuilder, ReaderThreads, SyncMode} +//! }; +//! +//! # fn main() -> Result<(), Box<dyn std::error::Error>> { +//! let db_dir = tempfile::tempdir()?; +//! +//! let config = ConfigBuilder::new() +//! // Use a custom database directory. +//! .db_directory(db_dir.path().to_path_buf()) +//! // Use as many reader threads as possible (when using `service`). +//! .reader_threads(ReaderThreads::OnePerThread) +//! // Use the fastest sync mode. +//! .sync_mode(SyncMode::Fast) +//! // Build into `Config` +//! .build(); +//! +//! // Start a database `service` using this configuration. +//! let (reader_handle, _) = cuprate_database::service::init(config.clone())?; +//! // It's using the config we provided. +//! assert_eq!(reader_handle.env().config(), &config); +//! # Ok(()) } +//! ``` + +mod config; +pub use config::{Config, ConfigBuilder}; + +mod reader_threads; +pub use reader_threads::ReaderThreads; + +mod sync_mode; +pub use sync_mode::SyncMode; diff --git a/storage/database/src/config/reader_threads.rs b/storage/database/src/config/reader_threads.rs new file mode 100644 index 00000000..34b20a88 --- /dev/null +++ b/storage/database/src/config/reader_threads.rs @@ -0,0 +1,189 @@ +//! Database [`Env`](crate::Env) configuration. +//! +//! This module contains the main [`Config`]uration struct +//! for the database [`Env`](crate::Env)ironment, and data +//! structures related to any configuration setting. +//! +//! These configurations are processed at runtime, meaning +//! the `Env` can/will dynamically adjust its behavior +//! based on these values. + +//---------------------------------------------------------------------------------------------------- Import +use std::num::NonZeroUsize; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +//---------------------------------------------------------------------------------------------------- ReaderThreads +/// Amount of database reader threads to spawn when using [`service`](crate::service). +/// +/// This controls how many reader thread `service`'s +/// thread-pool will spawn to receive and send requests/responses. +/// +/// It does nothing outside of `service`. +/// +/// It will always be at least 1, up until the amount of threads on the machine. +/// +/// The main function used to extract an actual +/// usable thread count out of this is [`ReaderThreads::as_threads`]. +#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ReaderThreads { + #[default] + /// Spawn 1 reader thread per available thread on the machine. + /// + /// For example, a `32-thread` system will spawn + /// `32` reader threads using this setting. + OnePerThread, + + /// Only spawn 1 reader thread. + One, + + /// Spawn a specified amount of reader threads. + /// + /// Note that no matter how large this value, it will be + /// ultimately capped at the amount of system threads. + /// + /// # `0` + /// `ReaderThreads::Number(0)` represents "use maximum value", + /// as such, it is equal to [`ReaderThreads::OnePerThread`]. + /// + /// ```rust + /// # use cuprate_database::config::*; + /// let reader_threads = ReaderThreads::from(0_usize); + /// assert!(matches!(reader_threads, ReaderThreads::OnePerThread)); + /// ``` + Number(usize), + + /// Spawn a specified % of reader threads. + /// + /// This must be a value in-between `0.0..1.0` + /// where `1.0` represents [`ReaderThreads::OnePerThread`]. + /// + /// # Example + /// For example, using a `16-core, 32-thread` Ryzen 5950x CPU: + /// + /// | Input | Total thread used | + /// |------------------------------------|-------------------| + /// | `ReaderThreads::Percent(0.0)` | 32 (maximum value) + /// | `ReaderThreads::Percent(0.5)` | 16 + /// | `ReaderThreads::Percent(0.75)` | 24 + /// | `ReaderThreads::Percent(1.0)` | 32 + /// | `ReaderThreads::Percent(2.0)` | 32 (saturating) + /// | `ReaderThreads::Percent(f32::NAN)` | 32 (non-normal default) + /// + /// # `0.0` + /// `ReaderThreads::Percent(0.0)` represents "use maximum value", + /// as such, it is equal to [`ReaderThreads::OnePerThread`]. + /// + /// # Not quite `0.0` + /// If the thread count multiplied by the percentage ends up being + /// non-zero, but not 1 thread, the minimum value 1 will be returned. + /// + /// ```rust + /// # use cuprate_database::config::*; + /// assert_eq!(ReaderThreads::Percent(0.000000001).as_threads().get(), 1); + /// ``` + Percent(f32), +} + +impl ReaderThreads { + /// This converts [`ReaderThreads`] into a safe, usable + /// number representing how many threads to spawn. + /// + /// This function will always return a number in-between `1..=total_thread_count`. + /// + /// It uses [`cuprate_helper::thread::threads()`] internally to determine the total thread count. + /// + /// # Example + /// ```rust + /// use cuprate_database::config::ReaderThreads as Rt; + /// + /// let total_threads: std::num::NonZeroUsize = + /// cuprate_helper::thread::threads(); + /// + /// assert_eq!(Rt::OnePerThread.as_threads(), total_threads); + /// + /// assert_eq!(Rt::One.as_threads().get(), 1); + /// + /// assert_eq!(Rt::Number(0).as_threads(), total_threads); + /// assert_eq!(Rt::Number(1).as_threads().get(), 1); + /// assert_eq!(Rt::Number(usize::MAX).as_threads(), total_threads); + /// + /// assert_eq!(Rt::Percent(0.01).as_threads().get(), 1); + /// assert_eq!(Rt::Percent(0.0).as_threads(), total_threads); + /// assert_eq!(Rt::Percent(1.0).as_threads(), total_threads); + /// assert_eq!(Rt::Percent(f32::NAN).as_threads(), total_threads); + /// assert_eq!(Rt::Percent(f32::INFINITY).as_threads(), total_threads); + /// assert_eq!(Rt::Percent(f32::NEG_INFINITY).as_threads(), total_threads); + /// + /// // Percentage only works on more than 1 thread. + /// if total_threads.get() > 1 { + /// assert_eq!( + /// Rt::Percent(0.5).as_threads().get(), + /// (total_threads.get() as f32 / 2.0) as usize, + /// ); + /// } + /// ``` + // + // INVARIANT: + // LMDB will error if we input zero, so don't allow that. + // <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L4687> + pub fn as_threads(&self) -> NonZeroUsize { + let total_threads = cuprate_helper::thread::threads(); + + match self { + Self::OnePerThread => total_threads, // use all threads + Self::One => NonZeroUsize::MIN, // one + Self::Number(n) => match NonZeroUsize::new(*n) { + Some(n) => std::cmp::min(n, total_threads), // saturate at total threads + None => total_threads, // 0 == maximum value + }, + + // We handle the casting loss. + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + Self::Percent(f) => { + // If non-normal float, use the default (all threads). + if !f.is_normal() || !(0.0..=1.0).contains(f) { + return total_threads; + } + + // 0.0 == maximum value. + if *f == 0.0 { + return total_threads; + } + + // Calculate percentage of total threads. + let thread_percent = (total_threads.get() as f32) * f; + match NonZeroUsize::new(thread_percent as usize) { + Some(n) => std::cmp::min(n, total_threads), // saturate at total threads. + None => { + // We checked for `0.0` above, so what this + // being 0 means that the percentage was _so_ + // low it made our thread count something like + // 0.99. In this case, just use 1 thread. + NonZeroUsize::MIN + } + } + } + } + } +} + +impl<T: Into<usize>> From<T> for ReaderThreads { + /// Create a [`ReaderThreads::Number`]. + /// + /// If `value` is `0`, this will return [`ReaderThreads::OnePerThread`]. + fn from(value: T) -> Self { + let u: usize = value.into(); + if u == 0 { + Self::OnePerThread + } else { + Self::Number(u) + } + } +} diff --git a/storage/database/src/config/sync_mode.rs b/storage/database/src/config/sync_mode.rs new file mode 100644 index 00000000..1d203396 --- /dev/null +++ b/storage/database/src/config/sync_mode.rs @@ -0,0 +1,135 @@ +//! Database [`Env`](crate::Env) configuration. +//! +//! This module contains the main [`Config`]uration struct +//! for the database [`Env`](crate::Env)ironment, and data +//! structures related to any configuration setting. +//! +//! These configurations are processed at runtime, meaning +//! the `Env` can/will dynamically adjust its behavior +//! based on these values. + +//---------------------------------------------------------------------------------------------------- Import + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +//---------------------------------------------------------------------------------------------------- SyncMode +/// Disk synchronization mode. +/// +/// This controls how/when the database syncs its data to disk. +/// +/// Regardless of the variant chosen, dropping [`Env`](crate::Env) +/// will always cause it to fully sync to disk. +/// +/// # Sync vs Async +/// All invariants except [`SyncMode::Async`] & [`SyncMode::Fast`] +/// are `synchronous`, as in the database will wait until the OS has +/// finished syncing all the data to disk before continuing. +/// +/// `SyncMode::Async` & `SyncMode::Fast` are `asynchronous`, meaning +/// the database will _NOT_ wait until the data is fully synced to disk +/// before continuing. Note that this doesn't mean the database itself +/// won't be synchronized between readers/writers, but rather that the +/// data _on disk_ may not be immediately synchronized after a write. +/// +/// Something like: +/// ```rust,ignore +/// db.put("key", value); +/// db.get("key"); +/// ``` +/// will be fine, most likely pulling from memory instead of disk. +/// +/// # SOMEDAY +/// Dynamic sync's are not yet supported. +/// +/// Only: +/// +/// - [`SyncMode::Safe`] +/// - [`SyncMode::Async`] +/// - [`SyncMode::Fast`] +/// +/// are supported, all other variants will panic on [`crate::Env::open`]. +#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SyncMode { + /// Use [`SyncMode::Fast`] until fully synced, + /// then use [`SyncMode::Safe`]. + /// + // # SOMEDAY: how to implement this? + // ref: <https://github.com/monero-project/monero/issues/1463> + // monerod-solution: <https://github.com/monero-project/monero/pull/1506> + // cuprate-issue: <https://github.com/Cuprate/cuprate/issues/78> + // + // We could: + // ```rust,ignore + // if current_db_block <= top_block.saturating_sub(N) { + // // don't sync() + // } else { + // // sync() + // } + // ``` + // where N is some threshold we pick that is _close_ enough + // to being synced where we want to start being safer. + // + // Essentially, when we are in a certain % range of being finished, + // switch to safe mode, until then, go fast. + FastThenSafe, + + #[default] + /// Fully sync to disk per transaction. + /// + /// Every database transaction commit will + /// fully sync all data to disk, _synchronously_, + /// so the database (writer) halts until synced. + /// + /// This is expected to be very slow. + /// + /// This matches: + /// - LMDB without any special sync flags + /// - [`redb::Durability::Immediate`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Immediate) + Safe, + + /// Asynchrously sync to disk per transaction. + /// + /// This is the same as [`SyncMode::Safe`], + /// but the syncs will be asynchronous, i.e. + /// each transaction commit will sync to disk, + /// but only eventually, not necessarily immediately. + /// + /// This matches: + /// - [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94) + /// - [`redb::Durability::Eventual`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Eventual) + Async, + + /// Fully sync to disk after we cross this transaction threshold. + /// + /// After committing [`usize`] amount of database + /// transactions, it will be sync to disk. + /// + /// `0` behaves the same as [`SyncMode::Safe`], and a ridiculously large + /// number like `usize::MAX` is practically the same as [`SyncMode::Fast`]. + Threshold(usize), + + /// Only flush at database shutdown. + /// + /// This is the fastest, yet unsafest option. + /// + /// It will cause the database to never _actively_ sync, + /// letting the OS decide when to flush data to disk. + /// + /// This matches: + /// - [`MDB_NOSYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#ga5791dd1adb09123f82dd1f331209e12e) + [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94) + /// - [`redb::Durability::None`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.None) + /// + /// `monerod` reference: <https://github.com/monero-project/monero/blob/7b7958bbd9d76375c47dc418b4adabba0f0b1785/src/blockchain_db/lmdb/db_lmdb.cpp#L1380-L1381> + /// + /// # Corruption + /// In the case of a system crash, the database + /// may become corrupted when using this option. + // + // FIXME: we could call this `unsafe` + // and use that terminology in the config file + // so users know exactly what they are getting + // themselves into. + Fast, +} diff --git a/storage/database/src/constants.rs b/storage/database/src/constants.rs new file mode 100644 index 00000000..667e36cb --- /dev/null +++ b/storage/database/src/constants.rs @@ -0,0 +1,86 @@ +//! General constants used throughout `cuprate-database`. + +//---------------------------------------------------------------------------------------------------- Import +use cfg_if::cfg_if; + +//---------------------------------------------------------------------------------------------------- Version +/// Current major version of the database. +/// +/// Returned by [`crate::ops::property::db_version`]. +/// +/// This is incremented by 1 when `cuprate_database`'s +/// structure/schema/tables change. +/// +/// This is akin to `VERSION` in `monerod`: +/// <https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/blockchain_db/lmdb/db_lmdb.cpp#L57> +pub const DATABASE_VERSION: u64 = 0; + +//---------------------------------------------------------------------------------------------------- Error Messages +/// Corrupt database error message. +/// +/// The error message shown to end-users in panic +/// messages if we think the database is corrupted. +/// +/// This is meant to be user-friendly. +pub const DATABASE_CORRUPT_MSG: &str = r"Cuprate has encountered a fatal error. The database may be corrupted. + +TODO: instructions on: +1. What to do +2. How to fix (re-sync, recover, etc) +3. General advice for preventing corruption +4. etc"; + +//---------------------------------------------------------------------------------------------------- Misc +/// Static string of the `crate` being used as the database backend. +/// +/// | Backend | Value | +/// |---------|-------| +/// | `heed` | `"heed"` +/// | `redb` | `"redb"` +pub const DATABASE_BACKEND: &str = { + cfg_if! { + if #[cfg(all(feature = "redb", not(feature = "heed")))] { + "redb" + } else { + "heed" + } + } +}; + +/// Cuprate's database filename. +/// +/// Used in [`Config::db_file`](crate::config::Config::db_file). +/// +/// | Backend | Value | +/// |---------|-------| +/// | `heed` | `"data.mdb"` +/// | `redb` | `"data.redb"` +pub const DATABASE_DATA_FILENAME: &str = { + cfg_if! { + if #[cfg(all(feature = "redb", not(feature = "heed")))] { + "data.redb" + } else { + "data.mdb" + } + } +}; + +/// Cuprate's database lock filename. +/// +/// | Backend | Value | +/// |---------|-------| +/// | `heed` | `Some("lock.mdb")` +/// | `redb` | `None` (redb doesn't use a file lock) +pub const DATABASE_LOCK_FILENAME: Option<&str> = { + cfg_if! { + if #[cfg(all(feature = "redb", not(feature = "heed")))] { + None + } else { + Some("lock.mdb") + } + } +}; + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test {} diff --git a/storage/database/src/database.rs b/storage/database/src/database.rs new file mode 100644 index 00000000..4a45f7cc --- /dev/null +++ b/storage/database/src/database.rs @@ -0,0 +1,216 @@ +//! Abstracted database table operations; `trait DatabaseRo` & `trait DatabaseRw`. + +//---------------------------------------------------------------------------------------------------- Import +use std::ops::RangeBounds; + +use crate::{error::RuntimeError, table::Table}; + +//---------------------------------------------------------------------------------------------------- DatabaseIter +/// Generic post-fix documentation for `DatabaseIter` methods. +macro_rules! doc_iter { + () => { + r"Although the returned iterator itself is tied to the lifetime +of `&self`, the returned values from the iterator are _owned_. + +# Errors +The construction of the iterator itself may error. + +Each iteration of the iterator has the potential to error as well." + }; +} + +/// Database (key-value store) read-only iteration abstraction. +/// +/// These are read-only iteration-related operations that +/// can only be called from [`DatabaseRo`] objects. +/// +/// # Hack +/// This is a HACK to get around the fact [`DatabaseRw`] tables +/// cannot safely return values returning lifetimes, as such, +/// only read-only tables implement this trait. +/// +/// - <https://github.com/Cuprate/cuprate/pull/102#discussion_r1548695610> +/// - <https://github.com/Cuprate/cuprate/pull/104> +pub trait DatabaseIter<T: Table> { + /// Get an [`Iterator`] of value's corresponding to a range of keys. + /// + /// For example: + /// ```rust,ignore + /// // This will return all 100 values corresponding + /// // to the keys `{0, 1, 2, ..., 100}`. + /// self.get_range(0..100); + /// ``` + /// + /// Although the returned iterator itself is tied to the lifetime + /// of `&'a self`, the returned values from the iterator are _owned_. + /// + #[doc = doc_iter!()] + fn get_range<'a, Range>( + &'a self, + range: Range, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + 'a, RuntimeError> + where + Range: RangeBounds<T::Key> + 'a; + + /// Get an [`Iterator`] that returns the `(key, value)` types for this database. + #[doc = doc_iter!()] + #[allow(clippy::iter_not_returning_iterator)] + fn iter( + &self, + ) -> Result<impl Iterator<Item = Result<(T::Key, T::Value), RuntimeError>> + '_, RuntimeError>; + + /// Get an [`Iterator`] that returns _only_ the `key` type for this database. + #[doc = doc_iter!()] + fn keys(&self) + -> Result<impl Iterator<Item = Result<T::Key, RuntimeError>> + '_, RuntimeError>; + + /// Get an [`Iterator`] that returns _only_ the `value` type for this database. + #[doc = doc_iter!()] + fn values( + &self, + ) -> Result<impl Iterator<Item = Result<T::Value, RuntimeError>> + '_, RuntimeError>; +} + +//---------------------------------------------------------------------------------------------------- DatabaseRo +/// Generic post-fix documentation for `DatabaseR{o,w}` methods. +macro_rules! doc_database { + () => { + r"# Errors +This will return [`RuntimeError::KeyNotFound`] if: +- Input does not exist OR +- Database is empty" + }; +} + +/// Database (key-value store) read abstraction. +/// +/// This is a read-only database table, +/// write operations are defined in [`DatabaseRw`]. +/// +/// # Safety +/// The table type that implements this MUST be `Send`. +/// +/// However if the table holds a reference to a transaction: +/// - only the transaction only has to be `Send` +/// - the table cannot implement `Send` +/// +/// For example: +/// +/// `heed`'s transactions are `Send` but `HeedTableRo` contains a `&` +/// to the transaction, as such, if `Send` were implemented on `HeedTableRo` +/// then 1 transaction could be used to open multiple tables, then sent to +/// other threads - this would be a soundness hole against `HeedTableRo`. +/// +/// `&T` is only `Send` if `T: Sync`. +/// +/// `heed::RoTxn: !Sync`, therefore our table +/// holding `&heed::RoTxn` must NOT be `Send`. +/// +/// - <https://doc.rust-lang.org/std/marker/trait.Sync.html> +/// - <https://doc.rust-lang.org/nomicon/send-and-sync.html> +pub unsafe trait DatabaseRo<T: Table> { + /// Get the value corresponding to a key. + #[doc = doc_database!()] + fn get(&self, key: &T::Key) -> Result<T::Value, RuntimeError>; + + /// Returns `true` if the database contains a value for the specified key. + /// + /// # Errors + /// Note that this will _never_ return `Err(RuntimeError::KeyNotFound)`, + /// as in that case, `Ok(false)` will be returned. + /// + /// Other errors may still occur. + fn contains(&self, key: &T::Key) -> Result<bool, RuntimeError> { + match self.get(key) { + Ok(_) => Ok(true), + Err(RuntimeError::KeyNotFound) => Ok(false), + Err(e) => Err(e), + } + } + + /// Returns the number of `(key, value)` pairs in the database. + /// + /// # Errors + /// This will never return [`RuntimeError::KeyNotFound`]. + fn len(&self) -> Result<u64, RuntimeError>; + + /// Returns the first `(key, value)` pair in the database. + #[doc = doc_database!()] + fn first(&self) -> Result<(T::Key, T::Value), RuntimeError>; + + /// Returns the last `(key, value)` pair in the database. + #[doc = doc_database!()] + fn last(&self) -> Result<(T::Key, T::Value), RuntimeError>; + + /// Returns `true` if the database contains no `(key, value)` pairs. + /// + /// # Errors + /// This can only return [`RuntimeError::Io`] on errors. + fn is_empty(&self) -> Result<bool, RuntimeError>; +} + +//---------------------------------------------------------------------------------------------------- DatabaseRw +/// Database (key-value store) read/write abstraction. +/// +/// All [`DatabaseRo`] functions are also callable by [`DatabaseRw`]. +pub trait DatabaseRw<T: Table>: DatabaseRo<T> { + /// Insert a key-value pair into the database. + /// + /// This will overwrite any existing key-value pairs. + /// + #[doc = doc_database!()] + /// + /// This will never [`RuntimeError::KeyExists`]. + fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError>; + + /// Delete a key-value pair in the database. + /// + /// This will return `Ok(())` if the key does not exist. + /// + #[doc = doc_database!()] + /// + /// This will never [`RuntimeError::KeyExists`]. + fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError>; + + /// Delete and return a key-value pair in the database. + /// + /// This is the same as [`DatabaseRw::delete`], however, + /// it will serialize the `T::Value` and return it. + /// + #[doc = doc_database!()] + fn take(&mut self, key: &T::Key) -> Result<T::Value, RuntimeError>; + + /// Fetch the value, and apply a function to it - or delete the entry. + /// + /// This will call [`DatabaseRo::get`] and call your provided function `f` on it. + /// + /// The [`Option`] `f` returns will dictate whether `update()`: + /// - Updates the current value OR + /// - Deletes the `(key, value)` pair + /// + /// - If `f` returns `Some(value)`, that will be [`DatabaseRw::put`] as the new value + /// - If `f` returns `None`, the entry will be [`DatabaseRw::delete`]d + /// + #[doc = doc_database!()] + fn update<F>(&mut self, key: &T::Key, mut f: F) -> Result<(), RuntimeError> + where + F: FnMut(T::Value) -> Option<T::Value>, + { + let value = DatabaseRo::get(self, key)?; + + match f(value) { + Some(value) => DatabaseRw::put(self, key, &value), + None => DatabaseRw::delete(self, key), + } + } + + /// Removes and returns the first `(key, value)` pair in the database. + /// + #[doc = doc_database!()] + fn pop_first(&mut self) -> Result<(T::Key, T::Value), RuntimeError>; + + /// Removes and returns the last `(key, value)` pair in the database. + /// + #[doc = doc_database!()] + fn pop_last(&mut self) -> Result<(T::Key, T::Value), RuntimeError>; +} diff --git a/storage/database/src/env.rs b/storage/database/src/env.rs new file mode 100644 index 00000000..3a32666b --- /dev/null +++ b/storage/database/src/env.rs @@ -0,0 +1,286 @@ +//! Abstracted database environment; `trait Env`. + +//---------------------------------------------------------------------------------------------------- Import +use std::num::NonZeroUsize; + +use crate::{ + config::Config, + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + error::{InitError, RuntimeError}, + resize::ResizeAlgorithm, + table::Table, + tables::{call_fn_on_all_tables_or_early_return, TablesIter, TablesMut}, + transaction::{TxRo, TxRw}, +}; + +//---------------------------------------------------------------------------------------------------- Env +/// Database environment abstraction. +/// +/// Essentially, the functions that can be called on [`ConcreteEnv`](crate::ConcreteEnv). +/// +/// # `Drop` +/// Objects that implement [`Env`] _should_ probably +/// [`Env::sync`] in their drop implementations, +/// although, no invariant relies on this (yet). +/// +/// # Lifetimes +/// The lifetimes associated with `Env` have a sequential flow: +/// 1. `ConcreteEnv` +/// 2. `'env` +/// 3. `'tx` +/// 4. `'db` +/// +/// As in: +/// - open database tables only live as long as... +/// - transactions which only live as long as the... +/// - environment ([`EnvInner`]) +pub trait Env: Sized { + //------------------------------------------------ Constants + /// Does the database backend need to be manually + /// resized when the memory-map is full? + /// + /// # Invariant + /// If this is `false`, that means this [`Env`] + /// must _never_ return a [`RuntimeError::ResizeNeeded`]. + /// + /// If this is `true`, [`Env::resize_map`] & [`Env::current_map_size`] + /// _must_ be re-implemented, as it just panics by default. + const MANUAL_RESIZE: bool; + + /// Does the database backend forcefully sync/flush + /// to disk on every transaction commit? + /// + /// This is used as an optimization. + const SYNCS_PER_TX: bool; + + //------------------------------------------------ Types + /// The struct representing the actual backend's database environment. + /// + /// This is used as the `self` in [`EnvInner`] functions, so whatever + /// this type is, is what will be accessible from those functions. + /// + // # HACK + // For `heed`, this is just `heed::Env`, for `redb` this is + // `(redb::Database, redb::Durability)` as each transaction + // needs the sync mode set during creation. + type EnvInner<'env>: EnvInner<'env, Self::TxRo<'env>, Self::TxRw<'env>> + where + Self: 'env; + + /// The read-only transaction type of the backend. + type TxRo<'env>: TxRo<'env> + 'env + where + Self: 'env; + + /// The read/write transaction type of the backend. + type TxRw<'env>: TxRw<'env> + 'env + where + Self: 'env; + + //------------------------------------------------ Required + /// Open the database environment, using the passed [`Config`]. + /// + /// # Invariants + /// This function **must** create all tables listed in [`crate::tables`]. + /// + /// The rest of the functions depend on the fact + /// they already exist, or else they will panic. + /// + /// # Errors + /// This will error if the database could not be opened. + /// + /// This is the only [`Env`] function that will return + /// an [`InitError`] instead of a [`RuntimeError`]. + fn open(config: Config) -> Result<Self, InitError>; + + /// Return the [`Config`] that this database was [`Env::open`]ed with. + fn config(&self) -> &Config; + + /// Fully sync the database caches to disk. + /// + /// # Invariant + /// This must **fully** and **synchronously** flush the database data to disk. + /// + /// I.e., after this function returns, there must be no doubts + /// that the data isn't synced yet, it _must_ be synced. + /// + // FIXME: either this invariant or `sync()` itself will most + // likely be removed/changed after `SyncMode` is finalized. + /// + /// # Errors + /// If there is a synchronization error, this should return an error. + fn sync(&self) -> Result<(), RuntimeError>; + + /// Resize the database's memory map to a + /// new (bigger) size using a [`ResizeAlgorithm`]. + /// + /// By default, this function will use the `ResizeAlgorithm` in [`Env::config`]. + /// + /// If `resize_algorithm` is `Some`, that will be used instead. + /// + /// This function returns the _new_ memory map size in bytes. + /// + /// # Invariant + /// This function _must_ be re-implemented if [`Env::MANUAL_RESIZE`] is `true`. + /// + /// Otherwise, this function will panic with `unreachable!()`. + #[allow(unused_variables)] + fn resize_map(&self, resize_algorithm: Option<ResizeAlgorithm>) -> NonZeroUsize { + unreachable!() + } + + /// What is the _current_ size of the database's memory map in bytes? + /// + /// # Invariant + /// 1. This function _must_ be re-implemented if [`Env::MANUAL_RESIZE`] is `true`. + /// 2. This function must be accurate, as [`Env::resize_map()`] may depend on it. + fn current_map_size(&self) -> usize { + unreachable!() + } + + /// Return the [`Env::EnvInner`]. + /// + /// # Locking behavior + /// When using the `heed` backend, [`Env::EnvInner`] is a + /// `RwLockReadGuard`, i.e., calling this function takes a + /// read lock on the `heed::Env`. + /// + /// Be aware of this, as other functions (currently only + /// [`Env::resize_map`]) will take a _write_ lock. + fn env_inner(&self) -> Self::EnvInner<'_>; + + //------------------------------------------------ Provided + /// Return the amount of actual of bytes the database is taking up on disk. + /// + /// This is the current _disk_ value in bytes, not the memory map. + /// + /// # Errors + /// This will error if either: + /// + /// - [`std::fs::File::open`] + /// - [`std::fs::File::metadata`] + /// + /// failed on the database file on disk. + fn disk_size_bytes(&self) -> std::io::Result<u64> { + // We have the direct PATH to the file, + // no need to use backend-specific functions. + // + // SAFETY: as we are only accessing the metadata of + // the file and not reading the bytes, it should be + // fine even with a memory mapped file being actively + // written to. + Ok(std::fs::File::open(&self.config().db_file)? + .metadata()? + .len()) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseRo +/// Document errors when opening tables in [`EnvInner`]. +macro_rules! doc_table_error { + () => { + r"# Errors +This will only return [`RuntimeError::Io`] if it errors. + +As all tables are created upon [`Env::open`], +this function will never error because a table doesn't exist." + }; +} + +/// The inner [`Env`] type. +/// +/// This type is created with [`Env::env_inner`] and represents +/// the type able to generate transactions and open tables. +/// +/// # Locking behavior +/// As noted in `Env::env_inner`, this is a `RwLockReadGuard` +/// when using the `heed` backend, be aware of this and do +/// not hold onto an `EnvInner` for a long time. +pub trait EnvInner<'env, Ro, Rw> +where + Self: 'env, + Ro: TxRo<'env>, + Rw: TxRw<'env>, +{ + /// Create a read-only transaction. + /// + /// # Errors + /// This will only return [`RuntimeError::Io`] if it errors. + fn tx_ro(&'env self) -> Result<Ro, RuntimeError>; + + /// Create a read/write transaction. + /// + /// # Errors + /// This will only return [`RuntimeError::Io`] if it errors. + fn tx_rw(&'env self) -> Result<Rw, RuntimeError>; + + /// Open a database in read-only mode. + /// + /// The returned value can have [`DatabaseRo`] + /// & [`DatabaseIter`] functions called on it. + /// + /// This will open the database [`Table`] + /// passed as a generic to this function. + /// + /// ```rust,ignore + /// let db = env.open_db_ro::<Table>(&tx_ro); + /// // ^ ^ + /// // database table table metadata + /// // (name, key/value type) + /// ``` + /// + #[doc = doc_table_error!()] + fn open_db_ro<T: Table>( + &self, + tx_ro: &Ro, + ) -> Result<impl DatabaseRo<T> + DatabaseIter<T>, RuntimeError>; + + /// Open a database in read/write mode. + /// + /// All [`DatabaseRo`] functions are also callable + /// with the returned [`DatabaseRw`] structure. + /// + /// Note that [`DatabaseIter`] functions are _not_ + /// available to [`DatabaseRw`] structures. + /// + /// This will open the database [`Table`] + /// passed as a generic to this function. + /// + #[doc = doc_table_error!()] + fn open_db_rw<T: Table>(&self, tx_rw: &Rw) -> Result<impl DatabaseRw<T>, RuntimeError>; + + /// Open all tables in read/iter mode. + /// + /// This calls [`EnvInner::open_db_ro`] on all database tables + /// and returns a structure that allows access to all tables. + /// + #[doc = doc_table_error!()] + fn open_tables(&self, tx_ro: &Ro) -> Result<impl TablesIter, RuntimeError> { + call_fn_on_all_tables_or_early_return! { + Self::open_db_ro(self, tx_ro) + } + } + + /// Open all tables in read-write mode. + /// + /// This calls [`EnvInner::open_db_rw`] on all database tables + /// and returns a structure that allows access to all tables. + /// + #[doc = doc_table_error!()] + fn open_tables_mut(&self, tx_rw: &Rw) -> Result<impl TablesMut, RuntimeError> { + call_fn_on_all_tables_or_early_return! { + Self::open_db_rw(self, tx_rw) + } + } + + /// Clear all `(key, value)`'s from a database table. + /// + /// This will delete all key and values in the passed + /// `T: Table`, but the table itself will continue to exist. + /// + /// Note that this operation is tied to `tx_rw`, as such this + /// function's effects can be aborted using [`TxRw::abort`]. + /// + #[doc = doc_table_error!()] + fn clear_db<T: Table>(&self, tx_rw: &mut Rw) -> Result<(), RuntimeError>; +} diff --git a/storage/database/src/error.rs b/storage/database/src/error.rs new file mode 100644 index 00000000..e47634f6 --- /dev/null +++ b/storage/database/src/error.rs @@ -0,0 +1,94 @@ +//! Database error types. + +//---------------------------------------------------------------------------------------------------- Import +use std::fmt::Debug; + +//---------------------------------------------------------------------------------------------------- Types +/// Alias for a thread-safe boxed error. +type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>; + +//---------------------------------------------------------------------------------------------------- InitError +/// Errors that occur during ([`Env::open`](crate::env::Env::open)). +/// +/// # Handling +/// As this is a database initialization error, the correct +/// way to handle any of these occurring is probably just to +/// exit the program. +/// +/// There is not much we as Cuprate can do +/// to recover from any of these errors. +#[derive(thiserror::Error, Debug)] +pub enum InitError { + /// The given `Path/File` existed and was accessible, + /// but was not a valid database file. + #[error("database file exists but is not valid")] + Invalid, + + /// The given `Path/File` existed, was a valid + /// database, but the version is incorrect. + #[error("database file is valid, but version is incorrect")] + InvalidVersion, + + /// I/O error. + #[error("database I/O error: {0}")] + Io(#[from] std::io::Error), + + /// The given `Path/File` existed, + /// was a valid database, but it is corrupt. + #[error("database file is corrupt")] + Corrupt, + + /// The database is currently in the process + /// of shutting down and cannot respond. + /// + /// # Notes + /// This error can only occur with the `heed` backend when + /// the database environment is opened _right_ at the same time + /// another thread/process is closing it. + /// + /// This will never occur with other backends. + #[error("database is shutting down")] + ShuttingDown, + + /// An unknown error occurred. + /// + /// This is for errors that cannot be recovered from, + /// but we'd still like to panic gracefully. + #[error("unknown error: {0}")] + Unknown(BoxError), +} + +//---------------------------------------------------------------------------------------------------- RuntimeError +/// Errors that occur _after_ successful ([`Env::open`](crate::env::Env::open)). +/// +/// There are no errors for: +/// 1. Missing tables +/// 2. (De)serialization +/// 3. Shutdown errors +/// +/// as `cuprate_database` upholds the invariant that: +/// +/// 1. All tables exist +/// 2. (De)serialization never fails +/// 3. The database (thread-pool) only shuts down when all channels are dropped +#[derive(thiserror::Error, Debug)] +pub enum RuntimeError { + /// The given key already existed in the database. + #[error("key already existed")] + KeyExists, + + /// The given key did not exist in the database. + #[error("key/value pair was not found")] + KeyNotFound, + + /// The database memory map is full and needs a resize. + /// + /// # Invariant + /// This error can only occur if [`Env::MANUAL_RESIZE`](crate::Env::MANUAL_RESIZE) is `true`. + #[error("database memory map must be resized")] + ResizeNeeded, + + /// A [`std::io::Error`]. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/storage/database/src/free.rs b/storage/database/src/free.rs new file mode 100644 index 00000000..7e145a28 --- /dev/null +++ b/storage/database/src/free.rs @@ -0,0 +1,11 @@ +//! General free functions (related to the database). + +//---------------------------------------------------------------------------------------------------- Import + +//---------------------------------------------------------------------------------------------------- Free functions + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/key.rs b/storage/database/src/key.rs new file mode 100644 index 00000000..13f7cede --- /dev/null +++ b/storage/database/src/key.rs @@ -0,0 +1,58 @@ +//! Database key abstraction; `trait Key`. + +//---------------------------------------------------------------------------------------------------- Import +use std::cmp::Ordering; + +use crate::storable::Storable; + +//---------------------------------------------------------------------------------------------------- Table +/// Database [`Table`](crate::table::Table) key metadata. +/// +/// Purely compile time information for database table keys. +// +// FIXME: this doesn't need to exist right now but +// may be used if we implement getting values using ranges. +// <https://github.com/Cuprate/cuprate/pull/117#discussion_r1589378104> +pub trait Key: Storable + Sized { + /// The primary key type. + type Primary: Storable; + + /// Compare 2 [`Key`]'s against each other. + /// + /// By default, this does a straight _byte_ comparison, + /// not a comparison of the key's value. + /// + /// ```rust + /// # use cuprate_database::*; + /// assert_eq!( + /// <u64 as Key>::compare([0].as_slice(), [1].as_slice()), + /// std::cmp::Ordering::Less, + /// ); + /// assert_eq!( + /// <u64 as Key>::compare([1].as_slice(), [1].as_slice()), + /// std::cmp::Ordering::Equal, + /// ); + /// assert_eq!( + /// <u64 as Key>::compare([2].as_slice(), [1].as_slice()), + /// std::cmp::Ordering::Greater, + /// ); + /// ``` + #[inline] + fn compare(left: &[u8], right: &[u8]) -> Ordering { + left.cmp(right) + } +} + +//---------------------------------------------------------------------------------------------------- Impl +impl<T> Key for T +where + T: Storable + Sized, +{ + type Primary = Self; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/lib.rs b/storage/database/src/lib.rs new file mode 100644 index 00000000..f1d2b2eb --- /dev/null +++ b/storage/database/src/lib.rs @@ -0,0 +1,301 @@ +//! Cuprate's database abstraction. +//! +//! This documentation is mostly for practical usage of `cuprate_database`. +//! +//! For a high-level overview, +//! see [`database/README.md`](https://github.com/Cuprate/cuprate/blob/main/database/README.md). +//! +//! # Purpose +//! This crate does 3 things: +//! 1. Abstracts various database backends with traits +//! 2. Implements various `Monero` related [operations](ops), [tables], and [types] +//! 3. Exposes a [`tower::Service`] backed by a thread-pool +//! +//! Each layer builds on-top of the previous. +//! +//! As a user of `cuprate_database`, consider using the higher-level [`service`] module, +//! or at the very least the [`ops`] module instead of interacting with the database traits directly. +//! +//! With that said, many database traits and internals (like [`DatabaseRo::get`]) are exposed. +//! +//! # Terminology +//! To be more clear on some terms used in this crate: +//! +//! | Term | Meaning | +//! |------------------|--------------------------------------| +//! | `Env` | The 1 database environment, the "whole" thing +//! | `DatabaseR{o,w}` | A _actively open_ readable/writable `key/value` store +//! | `Table` | Solely the metadata of a `Database` (the `key` and `value` types, and the name) +//! | `TxR{o,w}` | A read/write transaction +//! | `Storable` | A data that type can be stored in the database +//! +//! The dataflow is `Env` -> `Tx` -> `Database` +//! +//! Which reads as: +//! 1. You have a database `Environment` +//! 1. You open up a `Transaction` +//! 1. You open a particular `Table` from that `Environment`, getting a `Database` +//! 1. You can now read/write data from/to that `Database` +//! +//! # `ConcreteEnv` +//! This crate exposes [`ConcreteEnv`], which is a non-generic/non-dynamic, +//! concrete object representing a database [`Env`]ironment. +//! +//! The actual backend for this type is determined via feature flags. +//! +//! This object existing means `E: Env` doesn't need to be spread all through the codebase, +//! however, it also means some small invariants should be kept in mind. +//! +//! As `ConcreteEnv` is just a re-exposed type which has varying inner types, +//! it means some properties will change depending on the backend used. +//! +//! For example: +//! - [`std::mem::size_of::<ConcreteEnv>`] +//! - [`std::mem::align_of::<ConcreteEnv>`] +//! +//! Things like these functions are affected by the backend and inner data, +//! and should not be relied upon. This extends to any `struct/enum` that contains `ConcreteEnv`. +//! +//! `ConcreteEnv` invariants you can rely on: +//! - It implements [`Env`] +//! - Upon [`Drop::drop`], all database data will sync to disk +//! +//! Note that `ConcreteEnv` itself is not a clonable type, +//! it should be wrapped in [`std::sync::Arc`]. +//! +//! <!-- SOMEDAY: replace `ConcreteEnv` with `fn Env::open() -> impl Env`/ +//! and use `<E: Env>` everywhere it is stored instead. This would allow +//! generic-backed dynamic runtime selection of the database backend, i.e. +//! the user can select which database backend they use. --> +//! +//! # Feature flags +//! The `service` module requires the `service` feature to be enabled. +//! See the module for more documentation. +//! +//! Different database backends are enabled by the feature flags: +//! - `heed` (LMDB) +//! - `redb` +//! +//! The default is `heed`. +//! +//! `tracing` is always enabled and cannot be disabled via feature-flag. +//! <!-- FIXME: tracing should be behind a feature flag --> +//! +//! # Invariants when not using `service` +//! `cuprate_database` can be used without the `service` feature enabled but +//! there are some things that must be kept in mind when doing so. +//! +//! Failing to uphold these invariants may cause panics. +//! +//! 1. `LMDB` requires the user to resize the memory map resizing (see [`RuntimeError::ResizeNeeded`] +//! 1. `LMDB` has a maximum reader transaction count, currently it is set to `128` +//! 1. `LMDB` has [maximum key/value byte size](http://www.lmdb.tech/doc/group__internal.html#gac929399f5d93cef85f874b9e9b1d09e0) which must not be exceeded +//! +//! # Examples +//! The below is an example of using `cuprate_database`'s +//! lowest API, i.e. using the database directly. +//! +//! For examples of the higher-level APIs, see: +//! - [`ops`] +//! - [`service`] +//! +//! ```rust +//! use cuprate_database::{ +//! ConcreteEnv, +//! config::ConfigBuilder, +//! Env, EnvInner, +//! tables::{Tables, TablesMut}, +//! DatabaseRo, DatabaseRw, TxRo, TxRw, +//! }; +//! +//! # fn main() -> Result<(), Box<dyn std::error::Error>> { +//! // Create a configuration for the database environment. +//! let db_dir = tempfile::tempdir()?; +//! let config = ConfigBuilder::new() +//! .db_directory(db_dir.path().to_path_buf()) +//! .build(); +//! +//! // Initialize the database environment. +//! let env = ConcreteEnv::open(config)?; +//! +//! // Open up a transaction + tables for writing. +//! let env_inner = env.env_inner(); +//! let tx_rw = env_inner.tx_rw()?; +//! let mut tables = env_inner.open_tables_mut(&tx_rw)?; +//! +//! // ⚠️ Write data to the tables directly. +//! // (not recommended, use `ops` or `service`). +//! const KEY_IMAGE: [u8; 32] = [88; 32]; +//! tables.key_images_mut().put(&KEY_IMAGE, &())?; +//! +//! // Commit the data written. +//! drop(tables); +//! TxRw::commit(tx_rw)?; +//! +//! // Read the data, assert it is correct. +//! let tx_ro = env_inner.tx_ro()?; +//! let tables = env_inner.open_tables(&tx_ro)?; +//! let (key_image, _) = tables.key_images().first()?; +//! assert_eq!(key_image, KEY_IMAGE); +//! # Ok(()) } +//! ``` + +//---------------------------------------------------------------------------------------------------- Lints +// Forbid lints. +// Our code, and code generated (e.g macros) cannot overrule these. +#![forbid( + // `unsafe` is allowed but it _must_ be + // commented with `SAFETY: reason`. + clippy::undocumented_unsafe_blocks, + + // Never. + unused_unsafe, + redundant_semicolons, + unused_allocation, + coherence_leak_check, + while_true, + clippy::missing_docs_in_private_items, + + // Maybe can be put into `#[deny]`. + unconditional_recursion, + for_loops_over_fallibles, + unused_braces, + unused_labels, + keyword_idents, + non_ascii_idents, + variant_size_differences, + single_use_lifetimes, + + // Probably can be put into `#[deny]`. + future_incompatible, + let_underscore, + break_with_label_and_loop, + duplicate_macro_attributes, + exported_private_dependencies, + large_assignments, + overlapping_range_endpoints, + semicolon_in_expressions_from_macros, + noop_method_call, + unreachable_pub, +)] +// Deny lints. +// Some of these are `#[allow]`'ed on a per-case basis. +#![deny( + clippy::all, + clippy::correctness, + clippy::suspicious, + clippy::style, + clippy::complexity, + clippy::perf, + clippy::pedantic, + clippy::nursery, + clippy::cargo, + unused_doc_comments, + unused_mut, + missing_docs, + deprecated, + unused_comparisons, + nonstandard_style +)] +#![allow( + // FIXME: this lint affects crates outside of + // `database/` for some reason, allow for now. + clippy::cargo_common_metadata, + + // FIXME: adding `#[must_use]` onto everything + // might just be more annoying than useful... + // although it is sometimes nice. + clippy::must_use_candidate, + + // FIXME: good lint but too many false positives + // with our `Env` + `RwLock` setup. + clippy::significant_drop_tightening, + + // FIXME: good lint but is less clear in most cases. + clippy::items_after_statements, + + clippy::module_name_repetitions, + clippy::module_inception, + clippy::redundant_pub_crate, + clippy::option_if_let_else, +)] +// Allow some lints when running in debug mode. +#![cfg_attr(debug_assertions, allow(clippy::todo, clippy::multiple_crate_versions))] +// Allow some lints in tests. +#![cfg_attr( + test, + allow( + clippy::cognitive_complexity, + clippy::needless_pass_by_value, + clippy::cast_possible_truncation, + clippy::too_many_lines + ) +)] +// Only allow building 64-bit targets. +// +// This allows us to assume 64-bit +// invariants in code, e.g. `usize as u64`. +// +// # Safety +// As of 0d67bfb1bcc431e90c82d577bf36dd1182c807e2 (2024-04-12) +// there are invariants relying on 64-bit pointer sizes. +#[cfg(not(target_pointer_width = "64"))] +compile_error!("Cuprate is only compatible with 64-bit CPUs"); + +//---------------------------------------------------------------------------------------------------- Public API +// Import private modules, export public types. +// +// Documentation for each module is located in the respective file. + +mod backend; +pub use backend::ConcreteEnv; + +pub mod config; + +mod constants; +pub use constants::{ + DATABASE_BACKEND, DATABASE_CORRUPT_MSG, DATABASE_DATA_FILENAME, DATABASE_LOCK_FILENAME, + DATABASE_VERSION, +}; + +mod database; +pub use database::{DatabaseIter, DatabaseRo, DatabaseRw}; + +mod env; +pub use env::{Env, EnvInner}; + +mod error; +pub use error::{InitError, RuntimeError}; + +pub(crate) mod free; + +pub mod resize; + +mod key; +pub use key::Key; + +mod storable; +pub use storable::{Storable, StorableBytes, StorableVec}; + +pub mod ops; + +mod table; +pub use table::Table; + +pub mod tables; + +pub mod types; + +mod transaction; +pub use transaction::{TxRo, TxRw}; + +//---------------------------------------------------------------------------------------------------- Feature-gated +#[cfg(feature = "service")] +pub mod service; + +//---------------------------------------------------------------------------------------------------- Private +#[cfg(test)] +pub(crate) mod tests; + +#[cfg(feature = "service")] // only needed in `service` for now +pub(crate) mod unsafe_sendable; diff --git a/storage/database/src/ops/block.rs b/storage/database/src/ops/block.rs new file mode 100644 index 00000000..4f16cfde --- /dev/null +++ b/storage/database/src/ops/block.rs @@ -0,0 +1,472 @@ +//! Blocks functions. + +//---------------------------------------------------------------------------------------------------- Import +use bytemuck::TransparentWrapper; +use monero_serai::block::Block; + +use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; +use cuprate_types::{ExtendedBlockHeader, VerifiedBlockInformation}; + +use crate::{ + database::{DatabaseRo, DatabaseRw}, + error::RuntimeError, + ops::{ + blockchain::{chain_height, cumulative_generated_coins}, + macros::doc_error, + output::get_rct_num_outputs, + tx::{add_tx, remove_tx}, + }, + tables::{BlockHeights, BlockInfos, Tables, TablesMut}, + types::{BlockHash, BlockHeight, BlockInfo}, + StorableVec, +}; + +//---------------------------------------------------------------------------------------------------- `add_block_*` +/// Add a [`VerifiedBlockInformation`] to the database. +/// +/// This extracts all the data from the input block and +/// maps/adds them to the appropriate database tables. +/// +#[doc = doc_error!()] +/// +/// # Panics +/// This function will panic if: +/// - `block.height > u32::MAX` (not normally possible) +/// - `block.height` is not != [`chain_height`] +/// +/// # Already exists +/// This function will operate normally even if `block` already +/// exists, i.e., this function will not return `Err` even if you +/// call this function infinitely with the same block. +// no inline, too big. +pub fn add_block( + block: &VerifiedBlockInformation, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + //------------------------------------------------------ Check preconditions first + + // Cast height to `u32` for storage (handled at top of function). + // Panic (should never happen) instead of allowing DB corruption. + // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1560020991> + assert!( + u32::try_from(block.height).is_ok(), + "block.height ({}) > u32::MAX", + block.height, + ); + + let chain_height = chain_height(tables.block_heights())?; + assert_eq!( + block.height, chain_height, + "block.height ({}) != chain_height ({})", + block.height, chain_height, + ); + + // Expensive checks - debug only. + #[cfg(debug_assertions)] + { + assert_eq!(block.block.serialize(), block.block_blob); + assert_eq!(block.block.txs.len(), block.txs.len()); + for (i, tx) in block.txs.iter().enumerate() { + assert_eq!(tx.tx_blob, tx.tx.serialize()); + assert_eq!(tx.tx_hash, block.block.txs[i]); + } + } + + //------------------------------------------------------ Transaction / Outputs / Key Images + // Add the miner transaction first. + { + let tx = &block.block.miner_tx; + add_tx(tx, &tx.serialize(), &tx.hash(), &chain_height, tables)?; + } + + for tx in &block.txs { + add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &chain_height, tables)?; + } + + //------------------------------------------------------ Block Info + + // INVARIANT: must be below the above transaction loop since this + // RCT output count needs account for _this_ block's outputs. + let cumulative_rct_outs = get_rct_num_outputs(tables.rct_outputs())?; + + let cumulative_generated_coins = + cumulative_generated_coins(&block.height.saturating_sub(1), tables.block_infos())? + + block.generated_coins; + + let (cumulative_difficulty_low, cumulative_difficulty_high) = + split_u128_into_low_high_bits(block.cumulative_difficulty); + + // Block Info. + tables.block_infos_mut().put( + &block.height, + &BlockInfo { + cumulative_difficulty_low, + cumulative_difficulty_high, + cumulative_generated_coins, + cumulative_rct_outs, + timestamp: block.block.header.timestamp, + block_hash: block.block_hash, + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + weight: block.weight as u64, + long_term_weight: block.long_term_weight as u64, + }, + )?; + + // Block blobs. + tables + .block_blobs_mut() + .put(&block.height, StorableVec::wrap_ref(&block.block_blob))?; + + // Block heights. + tables + .block_heights_mut() + .put(&block.block_hash, &block.height)?; + + Ok(()) +} + +//---------------------------------------------------------------------------------------------------- `pop_block` +/// Remove the top/latest block from the database. +/// +/// The removed block's data is returned. +#[doc = doc_error!()] +/// +/// In `pop_block()`'s case, [`RuntimeError::KeyNotFound`] +/// will be returned if there are no blocks left. +// no inline, too big +pub fn pop_block( + tables: &mut impl TablesMut, +) -> Result<(BlockHeight, BlockHash, Block), RuntimeError> { + //------------------------------------------------------ Block Info + // Remove block data from tables. + let (block_height, block_hash) = { + let (block_height, block_info) = tables.block_infos_mut().pop_last()?; + (block_height, block_info.block_hash) + }; + + // Block heights. + tables.block_heights_mut().delete(&block_hash)?; + + // Block blobs. + // We deserialize the block blob into a `Block`, such + // that we can remove the associated transactions later. + let block_blob = tables.block_blobs_mut().take(&block_height)?.0; + let block = Block::read(&mut block_blob.as_slice())?; + + //------------------------------------------------------ Transaction / Outputs / Key Images + remove_tx(&block.miner_tx.hash(), tables)?; + for tx_hash in &block.txs { + remove_tx(tx_hash, tables)?; + } + + Ok((block_height, block_hash, block)) +} + +//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*` +/// Retrieve a [`ExtendedBlockHeader`] from the database. +/// +/// This extracts all the data from the database tables +/// needed to create a full `ExtendedBlockHeader`. +/// +/// # Notes +/// This is slightly more expensive than [`get_block_extended_header_from_height`] +/// (1 more database lookup). +#[doc = doc_error!()] +#[inline] +pub fn get_block_extended_header( + block_hash: &BlockHash, + tables: &impl Tables, +) -> Result<ExtendedBlockHeader, RuntimeError> { + get_block_extended_header_from_height(&tables.block_heights().get(block_hash)?, tables) +} + +/// Same as [`get_block_extended_header`] but with a [`BlockHeight`]. +#[doc = doc_error!()] +#[inline] +pub fn get_block_extended_header_from_height( + block_height: &BlockHeight, + tables: &impl Tables, +) -> Result<ExtendedBlockHeader, RuntimeError> { + let block_info = tables.block_infos().get(block_height)?; + let block_blob = tables.block_blobs().get(block_height)?.0; + let block = Block::read(&mut block_blob.as_slice())?; + + let cumulative_difficulty = combine_low_high_bits_to_u128( + block_info.cumulative_difficulty_low, + block_info.cumulative_difficulty_high, + ); + + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + #[allow(clippy::cast_possible_truncation)] + Ok(ExtendedBlockHeader { + cumulative_difficulty, + version: block.header.major_version, + vote: block.header.minor_version, + timestamp: block.header.timestamp, + block_weight: block_info.weight as usize, + long_term_weight: block_info.long_term_weight as usize, + }) +} + +/// Return the top/latest [`ExtendedBlockHeader`] from the database. +#[doc = doc_error!()] +#[inline] +pub fn get_block_extended_header_top( + tables: &impl Tables, +) -> Result<(ExtendedBlockHeader, BlockHeight), RuntimeError> { + let height = chain_height(tables.block_heights())?.saturating_sub(1); + let header = get_block_extended_header_from_height(&height, tables)?; + Ok((header, height)) +} + +//---------------------------------------------------------------------------------------------------- Misc +/// Retrieve a [`BlockInfo`] via its [`BlockHeight`]. +#[doc = doc_error!()] +#[inline] +pub fn get_block_info( + block_height: &BlockHeight, + table_block_infos: &impl DatabaseRo<BlockInfos>, +) -> Result<BlockInfo, RuntimeError> { + table_block_infos.get(block_height) +} + +/// Retrieve a [`BlockHeight`] via its [`BlockHash`]. +#[doc = doc_error!()] +#[inline] +pub fn get_block_height( + block_hash: &BlockHash, + table_block_heights: &impl DatabaseRo<BlockHeights>, +) -> Result<BlockHeight, RuntimeError> { + table_block_heights.get(block_hash) +} + +/// Check if a block exists in the database. +/// +/// # Errors +/// Note that this will never return `Err(RuntimeError::KeyNotFound)`, +/// as in that case, `Ok(false)` will be returned. +/// +/// Other errors may still occur. +#[inline] +pub fn block_exists( + block_hash: &BlockHash, + table_block_heights: &impl DatabaseRo<BlockHeights>, +) -> Result<bool, RuntimeError> { + table_block_heights.contains(block_hash) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +#[allow( + clippy::significant_drop_tightening, + clippy::cognitive_complexity, + clippy::too_many_lines +)] +mod test { + use pretty_assertions::assert_eq; + + use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; + + use super::*; + use crate::{ + ops::tx::{get_tx, tx_exists}, + tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, + transaction::TxRw, + Env, EnvInner, + }; + + /// Tests all above block functions. + /// + /// Note that this doesn't test the correctness of values added, as the + /// functions have a pre-condition that the caller handles this. + /// + /// It simply tests if the proper tables are mutated, and if the data + /// stored and retrieved is the same. + #[test] + fn all_block_functions() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let mut blocks = [ + block_v1_tx2().clone(), + block_v9_tx3().clone(), + block_v16_tx0().clone(), + ]; + // HACK: `add_block()` asserts blocks with non-sequential heights + // cannot be added, to get around this, manually edit the block height. + for (height, block) in blocks.iter_mut().enumerate() { + block.height = height as u64; + assert_eq!(block.block.serialize(), block.block_blob); + } + let generated_coins_sum = blocks + .iter() + .map(|block| block.generated_coins) + .sum::<u64>(); + + // Add blocks. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + for block in &blocks { + // println!("add_block: {block:#?}"); + add_block(block, &mut tables).unwrap(); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + // Assert all reads are OK. + let block_hashes = { + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + // Assert only the proper tables were added to. + AssertTableLen { + block_infos: 3, + block_blobs: 3, + block_heights: 3, + key_images: 69, + num_outputs: 41, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 111, + prunable_tx_blobs: 0, + rct_outputs: 8, + tx_blobs: 8, + tx_ids: 8, + tx_heights: 8, + tx_unlock_time: 3, + } + .assert(&tables); + + // Check `cumulative` functions work. + assert_eq!( + cumulative_generated_coins(&2, tables.block_infos()).unwrap(), + generated_coins_sum, + ); + + // Both height and hash should result in getting the same data. + let mut block_hashes = vec![]; + for block in &blocks { + println!("blocks.iter(): hash: {}", hex::encode(block.block_hash)); + + let height = get_block_height(&block.block_hash, tables.block_heights()).unwrap(); + + println!("blocks.iter(): height: {height}"); + + assert!(block_exists(&block.block_hash, tables.block_heights()).unwrap()); + + let block_header_from_height = + get_block_extended_header_from_height(&height, &tables).unwrap(); + let block_header_from_hash = + get_block_extended_header(&block.block_hash, &tables).unwrap(); + + // Just an alias, these names are long. + let b1 = block_header_from_hash; + let b2 = block; + assert_eq!(b1, block_header_from_height); + assert_eq!(b1.version, b2.block.header.major_version); + assert_eq!(b1.vote, b2.block.header.minor_version); + assert_eq!(b1.timestamp, b2.block.header.timestamp); + assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty); + assert_eq!(b1.block_weight, b2.weight); + assert_eq!(b1.long_term_weight, b2.long_term_weight); + + block_hashes.push(block.block_hash); + + // Assert transaction reads are OK. + for (i, tx) in block.txs.iter().enumerate() { + println!("tx_hash: {:?}", hex::encode(tx.tx_hash)); + + assert!(tx_exists(&tx.tx_hash, tables.tx_ids()).unwrap()); + + let tx2 = get_tx(&tx.tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap(); + + assert_eq!(tx.tx_blob, tx2.serialize()); + assert_eq!(tx.tx_weight, tx2.weight()); + assert_eq!(tx.tx_hash, block.block.txs[i]); + assert_eq!(tx.tx_hash, tx2.hash()); + } + } + + block_hashes + }; + + { + let len = block_hashes.len(); + let hashes: Vec<String> = block_hashes.iter().map(hex::encode).collect(); + println!("block_hashes: len: {len}, hashes: {hashes:?}"); + } + + // Remove the blocks. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + for block_hash in block_hashes.into_iter().rev() { + println!("pop_block(): block_hash: {}", hex::encode(block_hash)); + + let (_popped_height, popped_hash, _popped_block) = pop_block(&mut tables).unwrap(); + + assert_eq!(block_hash, popped_hash); + + assert!(matches!( + get_block_extended_header(&block_hash, &tables), + Err(RuntimeError::KeyNotFound) + )); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + assert_all_tables_are_empty(&env); + } + + /// We should panic if: `block.height` > `u32::MAX` + #[test] + #[should_panic(expected = "block.height (4294967296) > u32::MAX")] + fn block_height_gt_u32_max() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut block = block_v9_tx3().clone(); + + block.height = u64::from(u32::MAX) + 1; + add_block(&block, &mut tables).unwrap(); + } + + /// We should panic if: `block.height` != the chain height + #[test] + #[should_panic( + expected = "assertion `left == right` failed: block.height (123) != chain_height (1)\n left: 123\n right: 1" + )] + fn block_height_not_chain_height() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut block = block_v9_tx3().clone(); + // HACK: `add_block()` asserts blocks with non-sequential heights + // cannot be added, to get around this, manually edit the block height. + block.height = 0; + + // OK, `0 == 0` + assert_eq!(block.height, 0); + add_block(&block, &mut tables).unwrap(); + + // FAIL, `123 != 1` + block.height = 123; + add_block(&block, &mut tables).unwrap(); + } +} diff --git a/storage/database/src/ops/blockchain.rs b/storage/database/src/ops/blockchain.rs new file mode 100644 index 00000000..ce9cd69d --- /dev/null +++ b/storage/database/src/ops/blockchain.rs @@ -0,0 +1,182 @@ +//! Blockchain functions - chain height, generated coins, etc. + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + database::DatabaseRo, + error::RuntimeError, + ops::macros::doc_error, + tables::{BlockHeights, BlockInfos}, + types::BlockHeight, +}; + +//---------------------------------------------------------------------------------------------------- Free Functions +/// Retrieve the height of the chain. +/// +/// This returns the chain-tip, not the [`top_block_height`]. +/// +/// For example: +/// - The blockchain has 0 blocks => this returns `0` +/// - The blockchain has 1 block (height 0) => this returns `1` +/// - The blockchain has 2 blocks (height 1) => this returns `2` +/// +/// So the height of a new block would be `chain_height()`. +#[doc = doc_error!()] +#[inline] +pub fn chain_height( + table_block_heights: &impl DatabaseRo<BlockHeights>, +) -> Result<BlockHeight, RuntimeError> { + table_block_heights.len() +} + +/// Retrieve the height of the top block. +/// +/// This returns the height of the top block, not the [`chain_height`]. +/// +/// For example: +/// - The blockchain has 0 blocks => this returns `Err(RuntimeError::KeyNotFound)` +/// - The blockchain has 1 block (height 0) => this returns `Ok(0)` +/// - The blockchain has 2 blocks (height 1) => this returns `Ok(1)` +/// +/// Note that in cases where no blocks have been written to the +/// database yet, an error is returned: `Err(RuntimeError::KeyNotFound)`. +/// +#[doc = doc_error!()] +#[inline] +pub fn top_block_height( + table_block_heights: &impl DatabaseRo<BlockHeights>, +) -> Result<BlockHeight, RuntimeError> { + match table_block_heights.len()? { + 0 => Err(RuntimeError::KeyNotFound), + height => Ok(height - 1), + } +} + +/// Check how many cumulative generated coins there have been until a certain [`BlockHeight`]. +/// +/// This returns the total amount of Monero generated up to `block_height` +/// (including the block itself) in atomic units. +/// +/// For example: +/// - on the genesis block `0`, this returns the amount block `0` generated +/// - on the next block `1`, this returns the amount block `0` and `1` generated +/// +/// If no blocks have been added and `block_height == 0` +/// (i.e., the cumulative generated coins before genesis block is being calculated), +/// this returns `Ok(0)`. +#[doc = doc_error!()] +#[inline] +pub fn cumulative_generated_coins( + block_height: &BlockHeight, + table_block_infos: &impl DatabaseRo<BlockInfos>, +) -> Result<u64, RuntimeError> { + match table_block_infos.get(block_height) { + Ok(block_info) => Ok(block_info.cumulative_generated_coins), + Err(RuntimeError::KeyNotFound) if block_height == &0 => Ok(0), + Err(e) => Err(e), + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + + use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; + + use super::*; + use crate::{ + ops::block::add_block, + tables::Tables, + tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, + transaction::TxRw, + Env, EnvInner, + }; + + /// Tests all above functions. + /// + /// Note that this doesn't test the correctness of values added, as the + /// functions have a pre-condition that the caller handles this. + /// + /// It simply tests if the proper tables are mutated, and if the data + /// stored and retrieved is the same. + #[test] + fn all_blockchain_functions() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let mut blocks = [ + block_v1_tx2().clone(), + block_v9_tx3().clone(), + block_v16_tx0().clone(), + ]; + let blocks_len = u64::try_from(blocks.len()).unwrap(); + + // Add blocks. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + assert!(matches!( + top_block_height(tables.block_heights()), + Err(RuntimeError::KeyNotFound), + )); + assert_eq!( + 0, + cumulative_generated_coins(&0, tables.block_infos()).unwrap() + ); + + for (i, block) in blocks.iter_mut().enumerate() { + let i = u64::try_from(i).unwrap(); + // HACK: `add_block()` asserts blocks with non-sequential heights + // cannot be added, to get around this, manually edit the block height. + block.height = i; + add_block(block, &mut tables).unwrap(); + } + + // Assert reads are correct. + AssertTableLen { + block_infos: 3, + block_blobs: 3, + block_heights: 3, + key_images: 69, + num_outputs: 41, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 111, + prunable_tx_blobs: 0, + rct_outputs: 8, + tx_blobs: 8, + tx_ids: 8, + tx_heights: 8, + tx_unlock_time: 3, + } + .assert(&tables); + + assert_eq!(blocks_len, chain_height(tables.block_heights()).unwrap()); + assert_eq!( + blocks_len - 1, + top_block_height(tables.block_heights()).unwrap() + ); + assert_eq!( + cumulative_generated_coins(&0, tables.block_infos()).unwrap(), + 14_535_350_982_449, + ); + assert_eq!( + cumulative_generated_coins(&1, tables.block_infos()).unwrap(), + 17_939_125_004_612, + ); + assert_eq!( + cumulative_generated_coins(&2, tables.block_infos()).unwrap(), + 18_539_125_004_612, + ); + assert!(matches!( + cumulative_generated_coins(&3, tables.block_infos()), + Err(RuntimeError::KeyNotFound), + )); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + } +} diff --git a/storage/database/src/ops/key_image.rs b/storage/database/src/ops/key_image.rs new file mode 100644 index 00000000..5d0786c3 --- /dev/null +++ b/storage/database/src/ops/key_image.rs @@ -0,0 +1,127 @@ +//! Key image functions. + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + database::{DatabaseRo, DatabaseRw}, + error::RuntimeError, + ops::macros::{doc_add_block_inner_invariant, doc_error}, + tables::KeyImages, + types::KeyImage, +}; + +//---------------------------------------------------------------------------------------------------- Key image functions +/// Add a [`KeyImage`] to the "spent" set in the database. +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn add_key_image( + key_image: &KeyImage, + table_key_images: &mut impl DatabaseRw<KeyImages>, +) -> Result<(), RuntimeError> { + table_key_images.put(key_image, &()) +} + +/// Remove a [`KeyImage`] from the "spent" set in the database. +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn remove_key_image( + key_image: &KeyImage, + table_key_images: &mut impl DatabaseRw<KeyImages>, +) -> Result<(), RuntimeError> { + table_key_images.delete(key_image) +} + +/// Check if a [`KeyImage`] exists - i.e. if it is "spent". +#[doc = doc_error!()] +#[inline] +pub fn key_image_exists( + key_image: &KeyImage, + table_key_images: &impl DatabaseRo<KeyImages>, +) -> Result<bool, RuntimeError> { + table_key_images.contains(key_image) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use hex_literal::hex; + + use super::*; + use crate::{ + tables::{Tables, TablesMut}, + tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, + transaction::TxRw, + Env, EnvInner, + }; + + /// Tests all above key-image functions. + /// + /// Note that this doesn't test the correctness of values added, as the + /// functions have a pre-condition that the caller handles this. + /// + /// It simply tests if the proper tables are mutated, and if the data + /// stored and retrieved is the same. + #[test] + fn all_key_image_functions() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let key_images = [ + hex!("be1c87fc8f958f68fbe346a18dfb314204dca7573f61aae14840b8037da5c286"), + hex!("c5e4a592c11f34a12e13516ab2883b7c580d47b286b8fe8b15d57d2a18ade275"), + hex!("93288b646f858edfb0997ae08d7c76f4599b04c127f108e8e69a0696ae7ba334"), + hex!("726e9e3d8f826d24811183f94ff53aeba766c9efe6274eb80806f69b06bfa3fc"), + ]; + + // Add. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + for key_image in &key_images { + println!("add_key_image(): {}", hex::encode(key_image)); + add_key_image(key_image, tables.key_images_mut()).unwrap(); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + // Assert all reads are OK. + { + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + // Assert only the proper tables were added to. + AssertTableLen { + key_images: tables.key_images().len().unwrap(), + ..Default::default() + } + .assert(&tables); + + for key_image in &key_images { + println!("key_image_exists(): {}", hex::encode(key_image)); + key_image_exists(key_image, tables.key_images()).unwrap(); + } + } + + // Remove. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + for key_image in key_images { + println!("remove_key_image(): {}", hex::encode(key_image)); + remove_key_image(&key_image, tables.key_images_mut()).unwrap(); + assert!(!key_image_exists(&key_image, tables.key_images()).unwrap()); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + assert_all_tables_are_empty(&env); + } +} diff --git a/storage/database/src/ops/macros.rs b/storage/database/src/ops/macros.rs new file mode 100644 index 00000000..b7cdba47 --- /dev/null +++ b/storage/database/src/ops/macros.rs @@ -0,0 +1,33 @@ +//! Macros. +//! +//! These generate repetitive documentation +//! for all the functions defined in `ops/`. + +//---------------------------------------------------------------------------------------------------- Documentation macros +/// Generate documentation for the required `# Error` section. +macro_rules! doc_error { + () => { + r#"# Errors +This function returns [`RuntimeError::KeyNotFound`] if the input (if applicable) doesn't exist or other `RuntimeError`'s on database errors."# + }; +} +pub(super) use doc_error; + +/// Generate `# Invariant` documentation for internal `fn`'s +/// that should be called directly with caution. +macro_rules! doc_add_block_inner_invariant { + () => { + r#"# ⚠️ Invariant ⚠️ +This function mainly exists to be used internally by the parent function [`crate::ops::block::add_block`]. + +`add_block()` makes sure all data related to the input is mutated, while +this function _does not_, it specifically mutates _particular_ tables. + +This is usually undesired - although this function is still available to call directly. + +When calling this function, ensure that either: +1. This effect (incomplete database mutation) is what is desired, or that... +2. ...the other tables will also be mutated to a correct state"# + }; +} +pub(super) use doc_add_block_inner_invariant; diff --git a/storage/database/src/ops/mod.rs b/storage/database/src/ops/mod.rs new file mode 100644 index 00000000..9f48bd65 --- /dev/null +++ b/storage/database/src/ops/mod.rs @@ -0,0 +1,110 @@ +//! Abstracted Monero database operations. +//! +//! This module contains many free functions that use the +//! traits in this crate to generically call Monero-related +//! database operations. +//! +//! # `impl Table` +//! `ops/` functions take [`Tables`](crate::tables::Tables) and +//! [`TablesMut`](crate::tables::TablesMut) directly - these are +//! _already opened_ database tables. +//! +//! As such, the function puts the responsibility +//! of transactions, tables, etc on the caller. +//! +//! This does mean these functions are mostly as lean +//! as possible, so calling them in a loop should be okay. +//! +//! # Atomicity +//! As transactions are handled by the _caller_ of these functions, +//! it is up to the caller to decide what happens if one them return +//! an error. +//! +//! To maintain atomicity, transactions should be [`abort`](crate::transaction::TxRw::abort)ed +//! if one of the functions failed. +//! +//! For example, if [`add_block()`](block::add_block) is called and returns an [`Err`], +//! `abort`ing the transaction that opened the input `TableMut` would reverse all tables +//! mutated by `add_block()` up until the error, leaving it in the state it was in before +//! `add_block()` was called. +//! +//! # Sub-functions +//! The main functions within this module are mostly within the [`block`] module. +//! +//! Practically speaking, you should only be using 2 functions for mutation: +//! - [`add_block`](block::add_block) +//! - [`pop_block`](block::pop_block) +//! +//! The `block` functions are "parent" functions, calling other +//! sub-functions such as [`add_output()`](output::add_output). +//! +//! `add_output()` itself only modifies output-related tables, while the `block` "parent" +//! functions (like `add_block` and `pop_block`) modify all tables required. +//! +//! `add_block()` makes sure all data related to the input is mutated, while +//! this sub-function _do not_, it specifically mutates _particular_ tables. +//! +//! When calling this sub-functions, ensure that either: +//! 1. This effect (incomplete database mutation) is what is desired, or that... +//! 2. ...the other tables will also be mutated to a correct state +//! +//! # Example +//! Simple usage of `ops`. +//! +//! ```rust +//! use hex_literal::hex; +//! +//! use cuprate_test_utils::data::block_v16_tx0; +//! +//! use cuprate_database::{ +//! ConcreteEnv, +//! config::ConfigBuilder, +//! Env, EnvInner, +//! tables::{Tables, TablesMut}, +//! DatabaseRo, DatabaseRw, TxRo, TxRw, +//! ops::block::{add_block, pop_block}, +//! }; +//! +//! # fn main() -> Result<(), Box<dyn std::error::Error>> { +//! // Create a configuration for the database environment. +//! let db_dir = tempfile::tempdir()?; +//! let config = ConfigBuilder::new() +//! .db_directory(db_dir.path().to_path_buf()) +//! .build(); +//! +//! // Initialize the database environment. +//! let env = ConcreteEnv::open(config)?; +//! +//! // Open up a transaction + tables for writing. +//! let env_inner = env.env_inner(); +//! let tx_rw = env_inner.tx_rw()?; +//! let mut tables = env_inner.open_tables_mut(&tx_rw)?; +//! +//! // Write a block to the database. +//! let mut block = block_v16_tx0().clone(); +//! # block.height = 0; +//! add_block(&block, &mut tables)?; +//! +//! // Commit the data written. +//! drop(tables); +//! TxRw::commit(tx_rw)?; +//! +//! // Read the data, assert it is correct. +//! let tx_rw = env_inner.tx_rw()?; +//! let mut tables = env_inner.open_tables_mut(&tx_rw)?; +//! let (height, hash, serai_block) = pop_block(&mut tables)?; +//! +//! assert_eq!(height, 0); +//! assert_eq!(serai_block, block.block); +//! assert_eq!(hash, hex!("43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428")); +//! # Ok(()) } +//! ``` + +pub mod block; +pub mod blockchain; +pub mod key_image; +pub mod output; +pub mod property; +pub mod tx; + +mod macros; diff --git a/storage/database/src/ops/output.rs b/storage/database/src/ops/output.rs new file mode 100644 index 00000000..5b7620e4 --- /dev/null +++ b/storage/database/src/ops/output.rs @@ -0,0 +1,371 @@ +//! Output functions. + +//---------------------------------------------------------------------------------------------------- Import +use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::CompressedEdwardsY, Scalar}; +use monero_serai::{transaction::Timelock, H}; + +use cuprate_helper::map::u64_to_timelock; +use cuprate_types::OutputOnChain; + +use crate::{ + database::{DatabaseRo, DatabaseRw}, + error::RuntimeError, + ops::macros::{doc_add_block_inner_invariant, doc_error}, + tables::{Outputs, RctOutputs, Tables, TablesMut, TxUnlockTime}, + types::{Amount, AmountIndex, Output, OutputFlags, PreRctOutputId, RctOutput}, +}; + +//---------------------------------------------------------------------------------------------------- Pre-RCT Outputs +/// Add a Pre-RCT [`Output`] to the database. +/// +/// Upon [`Ok`], this function returns the [`PreRctOutputId`] that +/// can be used to lookup the `Output` in [`get_output()`]. +/// +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn add_output( + amount: Amount, + output: &Output, + tables: &mut impl TablesMut, +) -> Result<PreRctOutputId, RuntimeError> { + // FIXME: this would be much better expressed with a + // `btree_map::Entry`-like API, fix `trait DatabaseRw`. + let num_outputs = match tables.num_outputs().get(&amount) { + // Entry with `amount` already exists. + Ok(num_outputs) => num_outputs, + // Entry with `amount` didn't exist, this is + // the 1st output with this amount. + Err(RuntimeError::KeyNotFound) => 0, + Err(e) => return Err(e), + }; + // Update the amount of outputs. + tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?; + + let pre_rct_output_id = PreRctOutputId { + amount, + // The new `amount_index` is the length of amount of outputs with same amount. + amount_index: num_outputs, + }; + + tables.outputs_mut().put(&pre_rct_output_id, output)?; + Ok(pre_rct_output_id) +} + +/// Remove a Pre-RCT [`Output`] from the database. +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn remove_output( + pre_rct_output_id: &PreRctOutputId, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + // Decrement the amount index by 1, or delete the entry out-right. + // FIXME: this would be much better expressed with a + // `btree_map::Entry`-like API, fix `trait DatabaseRw`. + tables + .num_outputs_mut() + .update(&pre_rct_output_id.amount, |num_outputs| { + // INVARIANT: Should never be 0. + if num_outputs == 1 { + None + } else { + Some(num_outputs - 1) + } + })?; + + // Delete the output data itself. + tables.outputs_mut().delete(pre_rct_output_id) +} + +/// Retrieve a Pre-RCT [`Output`] from the database. +#[doc = doc_error!()] +#[inline] +pub fn get_output( + pre_rct_output_id: &PreRctOutputId, + table_outputs: &impl DatabaseRo<Outputs>, +) -> Result<Output, RuntimeError> { + table_outputs.get(pre_rct_output_id) +} + +/// How many pre-RCT [`Output`]s are there? +/// +/// This returns the amount of pre-RCT outputs currently stored. +#[doc = doc_error!()] +#[inline] +pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> Result<u64, RuntimeError> { + table_outputs.len() +} + +//---------------------------------------------------------------------------------------------------- RCT Outputs +/// Add an [`RctOutput`] to the database. +/// +/// Upon [`Ok`], this function returns the [`AmountIndex`] that +/// can be used to lookup the `RctOutput` in [`get_rct_output()`]. +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn add_rct_output( + rct_output: &RctOutput, + table_rct_outputs: &mut impl DatabaseRw<RctOutputs>, +) -> Result<AmountIndex, RuntimeError> { + let amount_index = get_rct_num_outputs(table_rct_outputs)?; + table_rct_outputs.put(&amount_index, rct_output)?; + Ok(amount_index) +} + +/// Remove an [`RctOutput`] from the database. +#[doc = doc_add_block_inner_invariant!()] +#[doc = doc_error!()] +#[inline] +pub fn remove_rct_output( + amount_index: &AmountIndex, + table_rct_outputs: &mut impl DatabaseRw<RctOutputs>, +) -> Result<(), RuntimeError> { + table_rct_outputs.delete(amount_index) +} + +/// Retrieve an [`RctOutput`] from the database. +#[doc = doc_error!()] +#[inline] +pub fn get_rct_output( + amount_index: &AmountIndex, + table_rct_outputs: &impl DatabaseRo<RctOutputs>, +) -> Result<RctOutput, RuntimeError> { + table_rct_outputs.get(amount_index) +} + +/// How many [`RctOutput`]s are there? +/// +/// This returns the amount of RCT outputs currently stored. +#[doc = doc_error!()] +#[inline] +pub fn get_rct_num_outputs( + table_rct_outputs: &impl DatabaseRo<RctOutputs>, +) -> Result<u64, RuntimeError> { + table_rct_outputs.len() +} + +//---------------------------------------------------------------------------------------------------- Mapping functions +/// Map an [`Output`] to a [`cuprate_types::OutputOnChain`]. +#[doc = doc_error!()] +pub fn output_to_output_on_chain( + output: &Output, + amount: Amount, + table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>, +) -> Result<OutputOnChain, RuntimeError> { + // FIXME: implement lookup table for common values: + // <https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/ringct/rctOps.cpp#L322> + let commitment = ED25519_BASEPOINT_POINT + H() * Scalar::from(amount); + + let time_lock = if output + .output_flags + .contains(OutputFlags::NON_ZERO_UNLOCK_TIME) + { + u64_to_timelock(table_tx_unlock_time.get(&output.tx_idx)?) + } else { + Timelock::None + }; + + let key = CompressedEdwardsY::from_slice(&output.key) + .map(|y| y.decompress()) + .unwrap_or(None); + + Ok(OutputOnChain { + height: u64::from(output.height), + time_lock, + key, + commitment, + }) +} + +/// Map an [`RctOutput`] to a [`cuprate_types::OutputOnChain`]. +/// +/// # Panics +/// This function will panic if `rct_output`'s `commitment` fails to decompress +/// into a valid [`EdwardsPoint`](curve25519_dalek::edwards::EdwardsPoint). +/// +/// This should normally not happen as commitments that +/// are stored in the database should always be valid. +#[doc = doc_error!()] +pub fn rct_output_to_output_on_chain( + rct_output: &RctOutput, + table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>, +) -> Result<OutputOnChain, RuntimeError> { + // INVARIANT: Commitments stored are valid when stored by the database. + let commitment = CompressedEdwardsY::from_slice(&rct_output.commitment) + .unwrap() + .decompress() + .unwrap(); + + let time_lock = if rct_output + .output_flags + .contains(OutputFlags::NON_ZERO_UNLOCK_TIME) + { + u64_to_timelock(table_tx_unlock_time.get(&rct_output.tx_idx)?) + } else { + Timelock::None + }; + + let key = CompressedEdwardsY::from_slice(&rct_output.key) + .map(|y| y.decompress()) + .unwrap_or(None); + + Ok(OutputOnChain { + height: u64::from(rct_output.height), + time_lock, + key, + commitment, + }) +} + +/// Map an [`PreRctOutputId`] to an [`OutputOnChain`]. +/// +/// Note that this still support RCT outputs, in that case, [`PreRctOutputId::amount`] should be `0`. +#[doc = doc_error!()] +pub fn id_to_output_on_chain( + id: &PreRctOutputId, + tables: &impl Tables, +) -> Result<OutputOnChain, RuntimeError> { + // v2 transactions. + if id.amount == 0 { + let rct_output = get_rct_output(&id.amount_index, tables.rct_outputs())?; + let output_on_chain = rct_output_to_output_on_chain(&rct_output, tables.tx_unlock_time())?; + + Ok(output_on_chain) + } else { + // v1 transactions. + let output = get_output(id, tables.outputs())?; + let output_on_chain = + output_to_output_on_chain(&output, id.amount, tables.tx_unlock_time())?; + + Ok(output_on_chain) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tables::{Tables, TablesMut}, + tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, + types::OutputFlags, + Env, EnvInner, + }; + + use pretty_assertions::assert_eq; + + /// Dummy `Output`. + const OUTPUT: Output = Output { + key: [44; 32], + height: 0, + output_flags: OutputFlags::NON_ZERO_UNLOCK_TIME, + tx_idx: 0, + }; + + /// Dummy `RctOutput`. + const RCT_OUTPUT: RctOutput = RctOutput { + key: [88; 32], + height: 1, + output_flags: OutputFlags::empty(), + tx_idx: 1, + commitment: [100; 32], + }; + + /// Dummy `Amount` + const AMOUNT: Amount = 22; + + /// Tests all above output functions when only inputting `Output` data (no Block). + /// + /// Note that this doesn't test the correctness of values added, as the + /// functions have a pre-condition that the caller handles this. + /// + /// It simply tests if the proper tables are mutated, and if the data + /// stored and retrieved is the same. + #[test] + fn all_output_functions() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + // Assert length is correct. + assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0); + assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0); + + // Add outputs. + let pre_rct_output_id = add_output(AMOUNT, &OUTPUT, &mut tables).unwrap(); + let amount_index = add_rct_output(&RCT_OUTPUT, tables.rct_outputs_mut()).unwrap(); + + assert_eq!( + pre_rct_output_id, + PreRctOutputId { + amount: AMOUNT, + amount_index: 0, + } + ); + + // Assert all reads of the outputs are OK. + { + // Assert proper tables were added to. + AssertTableLen { + block_infos: 0, + block_blobs: 0, + block_heights: 0, + key_images: 0, + num_outputs: 1, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 1, + prunable_tx_blobs: 0, + rct_outputs: 1, + tx_blobs: 0, + tx_ids: 0, + tx_heights: 0, + tx_unlock_time: 0, + } + .assert(&tables); + + // Assert length is correct. + assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 1); + assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 1); + assert_eq!(1, tables.num_outputs().get(&AMOUNT).unwrap()); + + // Assert value is save after retrieval. + assert_eq!( + OUTPUT, + get_output(&pre_rct_output_id, tables.outputs()).unwrap(), + ); + + assert_eq!( + RCT_OUTPUT, + get_rct_output(&amount_index, tables.rct_outputs()).unwrap(), + ); + } + + // Remove the outputs. + { + remove_output(&pre_rct_output_id, &mut tables).unwrap(); + remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap(); + + // Assert value no longer exists. + assert!(matches!( + get_output(&pre_rct_output_id, tables.outputs()), + Err(RuntimeError::KeyNotFound) + )); + assert!(matches!( + get_rct_output(&amount_index, tables.rct_outputs()), + Err(RuntimeError::KeyNotFound) + )); + + // Assert length is correct. + assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0); + assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0); + } + + assert_all_tables_are_empty(&env); + } +} diff --git a/storage/database/src/ops/property.rs b/storage/database/src/ops/property.rs new file mode 100644 index 00000000..279c3552 --- /dev/null +++ b/storage/database/src/ops/property.rs @@ -0,0 +1,39 @@ +//! Database properties functions - version, pruning, etc. +//! +//! SOMEDAY: the database `properties` table is not yet implemented. + +//---------------------------------------------------------------------------------------------------- Import +use monero_pruning::PruningSeed; + +use crate::{error::RuntimeError, ops::macros::doc_error}; +//---------------------------------------------------------------------------------------------------- Free Functions +/// SOMEDAY +/// +#[doc = doc_error!()] +/// +/// # Example +/// ```rust +/// # use cuprate_database::{*, tables::*, ops::block::*, ops::tx::*}; +/// // SOMEDAY +/// ``` +#[inline] +pub const fn get_blockchain_pruning_seed() -> Result<PruningSeed, RuntimeError> { + // SOMEDAY: impl pruning. + // We need a DB properties table. + Ok(PruningSeed::NotPruned) +} + +/// SOMEDAY +/// +#[doc = doc_error!()] +/// +/// # Example +/// ```rust +/// # use cuprate_database::{*, tables::*, ops::block::*, ops::tx::*}; +/// // SOMEDAY +/// ``` +#[inline] +pub const fn db_version() -> Result<u64, RuntimeError> { + // SOMEDAY: We need a DB properties table. + Ok(crate::constants::DATABASE_VERSION) +} diff --git a/storage/database/src/ops/tx.rs b/storage/database/src/ops/tx.rs new file mode 100644 index 00000000..b4f2984b --- /dev/null +++ b/storage/database/src/ops/tx.rs @@ -0,0 +1,434 @@ +//! Transaction functions. + +//---------------------------------------------------------------------------------------------------- Import +use bytemuck::TransparentWrapper; +use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, Scalar}; +use monero_serai::transaction::{Input, Timelock, Transaction}; + +use crate::{ + database::{DatabaseRo, DatabaseRw}, + error::RuntimeError, + ops::{ + key_image::{add_key_image, remove_key_image}, + macros::{doc_add_block_inner_invariant, doc_error}, + output::{ + add_output, add_rct_output, get_rct_num_outputs, remove_output, remove_rct_output, + }, + }, + tables::{TablesMut, TxBlobs, TxIds}, + types::{BlockHeight, Output, OutputFlags, PreRctOutputId, RctOutput, TxHash, TxId}, + StorableVec, +}; + +//---------------------------------------------------------------------------------------------------- Private +/// Add a [`Transaction`] (and related data) to the database. +/// +/// The `block_height` is the block that this `tx` belongs to. +/// +/// Note that the caller's input is trusted implicitly and no checks +/// are done (in this function) whether the `block_height` is correct or not. +/// +#[doc = doc_add_block_inner_invariant!()] +/// +/// # Notes +/// This function is different from other sub-functions and slightly more similar to +/// [`add_block()`](crate::ops::block::add_block) in that it calls other sub-functions. +/// +/// This function calls: +/// - [`add_output()`] +/// - [`add_rct_output()`] +/// - [`add_key_image()`] +/// +/// Thus, after [`add_tx`], those values (outputs and key images) +/// will be added to database tables as well. +/// +/// # Panics +/// This function will panic if: +/// - `block.height > u32::MAX` (not normally possible) +#[doc = doc_error!()] +#[inline] +pub fn add_tx( + tx: &Transaction, + tx_blob: &Vec<u8>, + tx_hash: &TxHash, + block_height: &BlockHeight, + tables: &mut impl TablesMut, +) -> Result<TxId, RuntimeError> { + let tx_id = get_num_tx(tables.tx_ids_mut())?; + + //------------------------------------------------------ Transaction data + tables.tx_ids_mut().put(tx_hash, &tx_id)?; + tables.tx_heights_mut().put(&tx_id, block_height)?; + tables + .tx_blobs_mut() + .put(&tx_id, StorableVec::wrap_ref(tx_blob))?; + + //------------------------------------------------------ Timelocks + // Height/time is not differentiated via type, but rather: + // "height is any value less than 500_000_000 and timestamp is any value above" + // so the `u64/usize` is stored without any tag. + // + // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1558504285> + match tx.prefix.timelock { + Timelock::None => (), + Timelock::Block(height) => tables.tx_unlock_time_mut().put(&tx_id, &(height as u64))?, + Timelock::Time(time) => tables.tx_unlock_time_mut().put(&tx_id, &time)?, + } + + //------------------------------------------------------ Pruning + // SOMEDAY: implement pruning after `monero-serai` does. + // if let PruningSeed::Pruned(decompressed_pruning_seed) = get_blockchain_pruning_seed()? { + // SOMEDAY: what to store here? which table? + // } + + //------------------------------------------------------ + let Ok(height) = u32::try_from(*block_height) else { + panic!("add_tx(): block_height ({block_height}) > u32::MAX"); + }; + + //------------------------------------------------------ Key Images + // Is this a miner transaction? + // Which table we add the output data to depends on this. + // <https://github.com/monero-project/monero/blob/eac1b86bb2818ac552457380c9dd421fb8935e5b/src/blockchain_db/blockchain_db.cpp#L212-L216> + let mut miner_tx = false; + + // Key images. + for inputs in &tx.prefix.inputs { + match inputs { + // Key images. + Input::ToKey { key_image, .. } => { + add_key_image(key_image.compress().as_bytes(), tables.key_images_mut())?; + } + // This is a miner transaction, set it for later use. + Input::Gen(_) => miner_tx = true, + } + } + + //------------------------------------------------------ Outputs + // Output bit flags. + // Set to a non-zero bit value if the unlock time is non-zero. + let output_flags = match tx.prefix.timelock { + Timelock::None => OutputFlags::empty(), + Timelock::Block(_) | Timelock::Time(_) => OutputFlags::NON_ZERO_UNLOCK_TIME, + }; + + let mut amount_indices = Vec::with_capacity(tx.prefix.outputs.len()); + + for (i, output) in tx.prefix.outputs.iter().enumerate() { + let key = *output.key.as_bytes(); + + // Outputs with clear amounts. + let amount_index = if let Some(amount) = output.amount { + // RingCT (v2 transaction) miner outputs. + if miner_tx && tx.prefix.version == 2 { + // Create commitment. + // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1559489302> + // FIXME: implement lookup table for common values: + // <https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/ringct/rctOps.cpp#L322> + let commitment = (ED25519_BASEPOINT_POINT + + monero_serai::H() * Scalar::from(amount)) + .compress() + .to_bytes(); + + add_rct_output( + &RctOutput { + key, + height, + output_flags, + tx_idx: tx_id, + commitment, + }, + tables.rct_outputs_mut(), + )? + // Pre-RingCT outputs. + } else { + add_output( + amount, + &Output { + key, + height, + output_flags, + tx_idx: tx_id, + }, + tables, + )? + .amount_index + } + // RingCT outputs. + } else { + let commitment = tx.rct_signatures.base.commitments[i].compress().to_bytes(); + add_rct_output( + &RctOutput { + key, + height, + output_flags, + tx_idx: tx_id, + commitment, + }, + tables.rct_outputs_mut(), + )? + }; + + amount_indices.push(amount_index); + } // for each output + + tables + .tx_outputs_mut() + .put(&tx_id, &StorableVec(amount_indices))?; + + Ok(tx_id) +} + +/// Remove a transaction from the database with its [`TxHash`]. +/// +/// This returns the [`TxId`] and [`TxBlob`](crate::types::TxBlob) of the removed transaction. +/// +#[doc = doc_add_block_inner_invariant!()] +/// +/// # Notes +/// As mentioned in [`add_tx`], this function will call other sub-functions: +/// - [`remove_output()`] +/// - [`remove_rct_output()`] +/// - [`remove_key_image()`] +/// +/// Thus, after [`remove_tx`], those values (outputs and key images) +/// will be remove from database tables as well. +/// +#[doc = doc_error!()] +#[inline] +pub fn remove_tx( + tx_hash: &TxHash, + tables: &mut impl TablesMut, +) -> Result<(TxId, Transaction), RuntimeError> { + //------------------------------------------------------ Transaction data + let tx_id = tables.tx_ids_mut().take(tx_hash)?; + let tx_blob = tables.tx_blobs_mut().take(&tx_id)?; + tables.tx_heights_mut().delete(&tx_id)?; + tables.tx_outputs_mut().delete(&tx_id)?; + + //------------------------------------------------------ Pruning + // SOMEDAY: implement pruning after `monero-serai` does. + // table_prunable_hashes.delete(&tx_id)?; + // table_prunable_tx_blobs.delete(&tx_id)?; + // if let PruningSeed::Pruned(decompressed_pruning_seed) = get_blockchain_pruning_seed()? { + // SOMEDAY: what to remove here? which table? + // } + + //------------------------------------------------------ Unlock Time + match tables.tx_unlock_time_mut().delete(&tx_id) { + Ok(()) | Err(RuntimeError::KeyNotFound) => (), + // An actual error occurred, return. + Err(e) => return Err(e), + } + + //------------------------------------------------------ + // Refer to the inner transaction type from now on. + let tx = Transaction::read(&mut tx_blob.0.as_slice())?; + + //------------------------------------------------------ Key Images + // Is this a miner transaction? + let mut miner_tx = false; + for inputs in &tx.prefix.inputs { + match inputs { + // Key images. + Input::ToKey { key_image, .. } => { + remove_key_image(key_image.compress().as_bytes(), tables.key_images_mut())?; + } + // This is a miner transaction, set it for later use. + Input::Gen(_) => miner_tx = true, + } + } // for each input + + //------------------------------------------------------ Outputs + // Remove each output in the transaction. + for output in &tx.prefix.outputs { + // Outputs with clear amounts. + if let Some(amount) = output.amount { + // RingCT miner outputs. + if miner_tx && tx.prefix.version == 2 { + let amount_index = get_rct_num_outputs(tables.rct_outputs())? - 1; + remove_rct_output(&amount_index, tables.rct_outputs_mut())?; + // Pre-RingCT outputs. + } else { + let amount_index = tables.num_outputs_mut().get(&amount)? - 1; + remove_output( + &PreRctOutputId { + amount, + amount_index, + }, + tables, + )?; + } + // RingCT outputs. + } else { + let amount_index = get_rct_num_outputs(tables.rct_outputs())? - 1; + remove_rct_output(&amount_index, tables.rct_outputs_mut())?; + } + } // for each output + + Ok((tx_id, tx)) +} + +//---------------------------------------------------------------------------------------------------- `get_tx_*` +/// Retrieve a [`Transaction`] from the database with its [`TxHash`]. +#[doc = doc_error!()] +#[inline] +pub fn get_tx( + tx_hash: &TxHash, + table_tx_ids: &impl DatabaseRo<TxIds>, + table_tx_blobs: &impl DatabaseRo<TxBlobs>, +) -> Result<Transaction, RuntimeError> { + get_tx_from_id(&table_tx_ids.get(tx_hash)?, table_tx_blobs) +} + +/// Retrieve a [`Transaction`] from the database with its [`TxId`]. +#[doc = doc_error!()] +#[inline] +pub fn get_tx_from_id( + tx_id: &TxId, + table_tx_blobs: &impl DatabaseRo<TxBlobs>, +) -> Result<Transaction, RuntimeError> { + let tx_blob = table_tx_blobs.get(tx_id)?.0; + Ok(Transaction::read(&mut tx_blob.as_slice())?) +} + +//---------------------------------------------------------------------------------------------------- +/// How many [`Transaction`]s are there? +/// +/// This returns the amount of transactions currently stored. +/// +/// For example: +/// - 0 transactions exist => returns 0 +/// - 1 transactions exist => returns 1 +/// - 5 transactions exist => returns 5 +/// - etc +#[doc = doc_error!()] +#[inline] +pub fn get_num_tx(table_tx_ids: &impl DatabaseRo<TxIds>) -> Result<u64, RuntimeError> { + table_tx_ids.len() +} + +//---------------------------------------------------------------------------------------------------- +/// Check if a transaction exists in the database. +/// +/// Returns `true` if it does, else `false`. +#[doc = doc_error!()] +#[inline] +pub fn tx_exists( + tx_hash: &TxHash, + table_tx_ids: &impl DatabaseRo<TxIds>, +) -> Result<bool, RuntimeError> { + table_tx_ids.contains(tx_hash) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tables::Tables, + tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, + transaction::TxRw, + Env, EnvInner, + }; + use cuprate_test_utils::data::{tx_v1_sig0, tx_v1_sig2, tx_v2_rct3}; + use pretty_assertions::assert_eq; + + /// Tests all above tx functions when only inputting `Transaction` data (no Block). + #[test] + fn all_tx_functions() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + // Monero `Transaction`, not database tx. + let txs = [tx_v1_sig0(), tx_v1_sig2(), tx_v2_rct3()]; + + // Add transactions. + let tx_ids = { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let tx_ids = txs + .iter() + .map(|tx| { + println!("add_tx(): {tx:#?}"); + add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &0, &mut tables).unwrap() + }) + .collect::<Vec<TxId>>(); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + + tx_ids + }; + + // Assert all reads of the transactions are OK. + let tx_hashes = { + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + // Assert only the proper tables were added to. + AssertTableLen { + block_infos: 0, + block_blobs: 0, + block_heights: 0, + key_images: 4, // added to key images + pruned_tx_blobs: 0, + prunable_hashes: 0, + num_outputs: 9, + outputs: 10, // added to outputs + prunable_tx_blobs: 0, + rct_outputs: 2, + tx_blobs: 3, + tx_ids: 3, + tx_heights: 3, + tx_unlock_time: 1, // only 1 has a timelock + } + .assert(&tables); + + // Both from ID and hash should result in getting the same transaction. + let mut tx_hashes = vec![]; + for (i, tx_id) in tx_ids.iter().enumerate() { + println!("tx_ids.iter(): i: {i}, tx_id: {tx_id}"); + + let tx_get_from_id = get_tx_from_id(tx_id, tables.tx_blobs()).unwrap(); + let tx_hash = tx_get_from_id.hash(); + let tx_get = get_tx(&tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap(); + + println!("tx_ids.iter(): tx_get_from_id: {tx_get_from_id:#?}, tx_get: {tx_get:#?}"); + + assert_eq!(tx_get_from_id.hash(), tx_get.hash()); + assert_eq!(tx_get_from_id.hash(), txs[i].tx_hash); + assert_eq!(tx_get_from_id, tx_get); + assert_eq!(tx_get, txs[i].tx); + assert!(tx_exists(&tx_hash, tables.tx_ids()).unwrap()); + + tx_hashes.push(tx_hash); + } + + tx_hashes + }; + + // Remove the transactions. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + for tx_hash in tx_hashes { + println!("remove_tx(): tx_hash: {tx_hash:?}"); + + let (tx_id, _) = remove_tx(&tx_hash, &mut tables).unwrap(); + assert!(matches!( + get_tx_from_id(&tx_id, tables.tx_blobs()), + Err(RuntimeError::KeyNotFound) + )); + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + assert_all_tables_are_empty(&env); + } +} diff --git a/storage/database/src/resize.rs b/storage/database/src/resize.rs new file mode 100644 index 00000000..99d6d7e3 --- /dev/null +++ b/storage/database/src/resize.rs @@ -0,0 +1,307 @@ +//! Database memory map resizing algorithms. +//! +//! This modules contains [`ResizeAlgorithm`] which determines how the +//! [`ConcreteEnv`](crate::ConcreteEnv) resizes its memory map when needing more space. +//! This value is in [`Config`](crate::config::Config) and can be selected at runtime. +//! +//! Although, it is only used by `ConcreteEnv` if [`Env::MANUAL_RESIZE`](crate::env::Env::MANUAL_RESIZE) is `true`. +//! +//! The algorithms are available as free functions in this module as well. +//! +//! # Page size +//! All free functions in this module will +//! return a multiple of the OS page size ([`page_size()`]), +//! [LMDB will error](http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5) +//! if this is not the case. +//! +//! # Invariants +//! All returned [`NonZeroUsize`] values of the free functions in this module +//! (including [`ResizeAlgorithm::resize`]) uphold the following invariants: +//! 1. It will always be `>=` the input `current_size_bytes` +//! 2. It will always be a multiple of [`page_size()`] + +//---------------------------------------------------------------------------------------------------- Import +use std::{num::NonZeroUsize, sync::OnceLock}; + +//---------------------------------------------------------------------------------------------------- ResizeAlgorithm +/// The function/algorithm used by the +/// database when resizing the memory map. +/// +// # SOMEDAY +// We could test around with different algorithms. +// Calling `heed::Env::resize` is surprisingly fast, +// around `0.0000082s` on my machine. We could probably +// get away with smaller and more frequent resizes. +// **With the caveat being we are taking a `WriteGuard` to a `RwLock`.** +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ResizeAlgorithm { + /// Uses [`monero`]. + Monero, + + /// Uses [`fixed_bytes`]. + FixedBytes(NonZeroUsize), + + /// Uses [`percent`]. + Percent(f32), +} + +impl ResizeAlgorithm { + /// Returns [`Self::Monero`]. + /// + /// ```rust + /// # use cuprate_database::resize::*; + /// assert!(matches!(ResizeAlgorithm::new(), ResizeAlgorithm::Monero)); + /// ``` + #[inline] + pub const fn new() -> Self { + Self::Monero + } + + /// Maps the `self` variant to the free functions in [`crate::resize`]. + /// + /// This function returns the _new_ memory map size in bytes. + #[inline] + pub fn resize(&self, current_size_bytes: usize) -> NonZeroUsize { + match self { + Self::Monero => monero(current_size_bytes), + Self::FixedBytes(add_bytes) => fixed_bytes(current_size_bytes, add_bytes.get()), + Self::Percent(f) => percent(current_size_bytes, *f), + } + } +} + +impl Default for ResizeAlgorithm { + /// Calls [`Self::new`]. + /// + /// ```rust + /// # use cuprate_database::resize::*; + /// assert_eq!(ResizeAlgorithm::new(), ResizeAlgorithm::default()); + /// ``` + #[inline] + fn default() -> Self { + Self::new() + } +} + +//---------------------------------------------------------------------------------------------------- Free functions +/// This function retrieves the system’s memory page size. +/// +/// It is just [`page_size::get`](https://docs.rs/page_size) internally. +/// +/// This caches the result, so this function is cheap after the 1st call. +/// +/// # Panics +/// This function will panic if the OS returns of page size of `0` (impossible?). +#[inline] +pub fn page_size() -> NonZeroUsize { + /// Cached result of [`page_size()`]. + static PAGE_SIZE: OnceLock<NonZeroUsize> = OnceLock::new(); + *PAGE_SIZE + .get_or_init(|| NonZeroUsize::new(page_size::get()).expect("page_size::get() returned 0")) +} + +/// Memory map resize closely matching `monerod`. +/// +/// # Method +/// This function mostly matches `monerod`'s current resize implementation[^1], +/// and will increase `current_size_bytes` by `1 << 30`[^2] exactly then +/// rounded to the nearest multiple of the OS page size. +/// +/// [^1]: <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L549> +/// +/// [^2]: `1_073_745_920` +/// +/// ```rust +/// # use cuprate_database::resize::*; +/// // The value this function will increment by +/// // (assuming page multiple of 4096). +/// const N: usize = 1_073_741_824; +/// +/// // 0 returns the minimum value. +/// assert_eq!(monero(0).get(), N); +/// +/// // Rounds up to nearest OS page size. +/// assert_eq!(monero(1).get(), N + page_size().get()); +/// ``` +/// +/// # Panics +/// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`]. +/// +/// ```rust,should_panic +/// # use cuprate_database::resize::*; +/// // Ridiculous large numbers panic. +/// monero(usize::MAX); +/// ``` +pub fn monero(current_size_bytes: usize) -> NonZeroUsize { + /// The exact expression used by `monerod` + /// when calculating how many bytes to add. + /// + /// The nominal value is `1_073_741_824`. + /// Not actually 1 GB but close enough I guess. + /// + /// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L553> + const ADD_SIZE: usize = 1_usize << 30; + + let page_size = page_size().get(); + let new_size_bytes = current_size_bytes + ADD_SIZE; + + // Round up the new size to the + // nearest multiple of the OS page size. + let remainder = new_size_bytes % page_size; + + // INVARIANT: minimum is always at least `ADD_SIZE`. + NonZeroUsize::new(if remainder == 0 { + new_size_bytes + } else { + (new_size_bytes + page_size) - remainder + }) + .unwrap() +} + +/// Memory map resize by a fixed amount of bytes. +/// +/// # Method +/// This function will `current_size_bytes + add_bytes` +/// and then round up to nearest OS page size. +/// +/// ```rust +/// # use cuprate_database::resize::*; +/// let page_size: usize = page_size().get(); +/// +/// // Anything below the page size will round up to the page size. +/// for i in 0..=page_size { +/// assert_eq!(fixed_bytes(0, i).get(), page_size); +/// } +/// +/// // (page_size + 1) will round up to (page_size * 2). +/// assert_eq!(fixed_bytes(page_size, 1).get(), page_size * 2); +/// +/// // (page_size + page_size) doesn't require any rounding. +/// assert_eq!(fixed_bytes(page_size, page_size).get(), page_size * 2); +/// ``` +/// +/// # Panics +/// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`]. +/// +/// ```rust,should_panic +/// # use cuprate_database::resize::*; +/// // Ridiculous large numbers panic. +/// fixed_bytes(1, usize::MAX); +/// ``` +pub fn fixed_bytes(current_size_bytes: usize, add_bytes: usize) -> NonZeroUsize { + let page_size = page_size(); + let new_size_bytes = current_size_bytes + add_bytes; + + // Guard against < page_size. + if new_size_bytes <= page_size.get() { + return page_size; + } + + // Round up the new size to the + // nearest multiple of the OS page size. + let remainder = new_size_bytes % page_size; + + // INVARIANT: we guarded against < page_size above. + NonZeroUsize::new(if remainder == 0 { + new_size_bytes + } else { + (new_size_bytes + page_size.get()) - remainder + }) + .unwrap() +} + +/// Memory map resize by a percentage. +/// +/// # Method +/// This function will multiply `current_size_bytes` by `percent`. +/// +/// Any input `<= 1.0` or non-normal float ([`f32::NAN`], [`f32::INFINITY`]) +/// will make the returning `NonZeroUsize` the same as `current_size_bytes` +/// (rounded up to the OS page size). +/// +/// ```rust +/// # use cuprate_database::resize::*; +/// let page_size: usize = page_size().get(); +/// +/// // Anything below the page size will round up to the page size. +/// for i in 0..=page_size { +/// assert_eq!(percent(i, 1.0).get(), page_size); +/// } +/// +/// // Same for 2 page sizes. +/// for i in (page_size + 1)..=(page_size * 2) { +/// assert_eq!(percent(i, 1.0).get(), page_size * 2); +/// } +/// +/// // Weird floats do nothing. +/// assert_eq!(percent(page_size, f32::NAN).get(), page_size); +/// assert_eq!(percent(page_size, f32::INFINITY).get(), page_size); +/// assert_eq!(percent(page_size, f32::NEG_INFINITY).get(), page_size); +/// assert_eq!(percent(page_size, -1.0).get(), page_size); +/// assert_eq!(percent(page_size, 0.999).get(), page_size); +/// ``` +/// +/// # Panics +/// This function will panic if `current_size_bytes * percent` +/// is closer to [`usize::MAX`] than the OS page size. +/// +/// ```rust,should_panic +/// # use cuprate_database::resize::*; +/// // Ridiculous large numbers panic. +/// percent(usize::MAX, 1.001); +/// ``` +pub fn percent(current_size_bytes: usize, percent: f32) -> NonZeroUsize { + // Guard against bad floats. + use std::num::FpCategory; + let percent = match percent.classify() { + FpCategory::Normal => { + if percent <= 1.0 { + 1.0 + } else { + percent + } + } + _ => 1.0, + }; + + let page_size = page_size(); + + // INVARIANT: Allow `f32` <-> `usize` casting, we handle all cases. + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss + )] + let new_size_bytes = ((current_size_bytes as f32) * percent) as usize; + + // Panic if rounding up to the nearest page size would overflow. + let new_size_bytes = if new_size_bytes > (usize::MAX - page_size.get()) { + panic!("new_size_bytes is percent() near usize::MAX"); + } else { + new_size_bytes + }; + + // Guard against < page_size. + if new_size_bytes <= page_size.get() { + return page_size; + } + + // Round up the new size to the + // nearest multiple of the OS page size. + let remainder = new_size_bytes % page_size; + + // INVARIANT: we guarded against < page_size above. + NonZeroUsize::new(if remainder == 0 { + new_size_bytes + } else { + (new_size_bytes + page_size.get()) - remainder + }) + .unwrap() +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/service/free.rs b/storage/database/src/service/free.rs new file mode 100644 index 00000000..fb40a065 --- /dev/null +++ b/storage/database/src/service/free.rs @@ -0,0 +1,40 @@ +//! General free functions used (related to `cuprate_database::service`). + +//---------------------------------------------------------------------------------------------------- Import +use std::sync::Arc; + +use crate::{ + config::Config, + error::InitError, + service::{DatabaseReadHandle, DatabaseWriteHandle}, + ConcreteEnv, Env, +}; + +//---------------------------------------------------------------------------------------------------- Init +#[cold] +#[inline(never)] // Only called once (?) +/// Initialize a database & thread-pool, and return a read/write handle to it. +/// +/// Once the returned handles are [`Drop::drop`]ed, the reader +/// thread-pool and writer thread will exit automatically. +/// +/// # Errors +/// This will forward the error if [`Env::open`] failed. +pub fn init(config: Config) -> Result<(DatabaseReadHandle, DatabaseWriteHandle), InitError> { + let reader_threads = config.reader_threads; + + // Initialize the database itself. + let db = Arc::new(ConcreteEnv::open(config)?); + + // Spawn the Reader thread pool and Writer. + let readers = DatabaseReadHandle::init(&db, reader_threads); + let writer = DatabaseWriteHandle::init(db); + + Ok((readers, writer)) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/service/mod.rs b/storage/database/src/service/mod.rs new file mode 100644 index 00000000..ca5c9e6f --- /dev/null +++ b/storage/database/src/service/mod.rs @@ -0,0 +1,130 @@ +//! [`tower::Service`] integeration + thread-pool. +//! +//! ## `service` +//! The `service` module implements the [`tower`] integration, +//! along with the reader/writer thread-pool system. +//! +//! The thread-pool allows outside crates to communicate with it by +//! sending database [`Request`][req_r]s and receiving [`Response`][resp]s `async`hronously - +//! without having to actually worry and handle the database themselves. +//! +//! The system is managed by this crate, and only requires [`init`] by the user. +//! +//! This module must be enabled with the `service` feature. +//! +//! ## Handles +//! The 2 handles to the database are: +//! - [`DatabaseReadHandle`] +//! - [`DatabaseWriteHandle`] +//! +//! The 1st allows any caller to send [`ReadRequest`][req_r]s. +//! +//! The 2nd allows any caller to send [`WriteRequest`][req_w]s. +//! +//! The `DatabaseReadHandle` can be shared as it is cheaply [`Clone`]able, however, +//! the `DatabaseWriteHandle` cannot be cloned. There is only 1 place in Cuprate that +//! writes, so it is passed there and used. +//! +//! ## Initialization +//! The database & thread-pool system can be initialized with [`init()`]. +//! +//! This causes the underlying database/threads to be setup +//! and returns a read/write handle to that database. +//! +//! ## Shutdown +//! Upon the above handles being dropped, the corresponding thread(s) will automatically exit, i.e: +//! - The last [`DatabaseReadHandle`] is dropped => reader thread-pool exits +//! - The last [`DatabaseWriteHandle`] is dropped => writer thread exits +//! +//! Upon dropping the [`crate::ConcreteEnv`]: +//! - All un-processed database transactions are completed +//! - All data gets flushed to disk (caused by [`Drop::drop`] impl on [`crate::ConcreteEnv`]) +//! +//! ## Request and Response +//! To interact with the database (whether reading or writing data), +//! a `Request` can be sent using one of the above handles. +//! +//! Both the handles implement `tower::Service`, so they can be [`tower::Service::call`]ed. +//! +//! An `async`hronous channel will be returned from the call. +//! This channel can be `.await`ed upon to (eventually) receive +//! the corresponding `Response` to your `Request`. +//! +//! [req_r]: cuprate_types::service::ReadRequest +//! +//! [req_w]: cuprate_types::service::WriteRequest +//! +//! [resp]: cuprate_types::service::Response +//! +//! # Example +//! Simple usage of `service`. +//! +//! ```rust +//! use hex_literal::hex; +//! use tower::{Service, ServiceExt}; +//! +//! use cuprate_types::service::{ReadRequest, WriteRequest, Response}; +//! use cuprate_test_utils::data::block_v16_tx0; +//! +//! use cuprate_database::{ConcreteEnv, config::ConfigBuilder, Env}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box<dyn std::error::Error>> { +//! // Create a configuration for the database environment. +//! let db_dir = tempfile::tempdir()?; +//! let config = ConfigBuilder::new() +//! .db_directory(db_dir.path().to_path_buf()) +//! .build(); +//! +//! // Initialize the database thread-pool. +//! let (mut read_handle, mut write_handle) = cuprate_database::service::init(config)?; +//! +//! // Prepare a request to write block. +//! let mut block = block_v16_tx0().clone(); +//! # block.height = 0 as u64; // must be 0th height or panic in `add_block()` +//! let request = WriteRequest::WriteBlock(block); +//! +//! // Send the request. +//! // We receive back an `async` channel that will +//! // eventually yield the result when `service` +//! // is done writing the block. +//! let response_channel = write_handle.ready().await?.call(request); +//! +//! // Block write was OK. +//! let response = response_channel.await?; +//! assert_eq!(response, Response::WriteBlockOk); +//! +//! // Now, let's try getting the block hash +//! // of the block we just wrote. +//! let request = ReadRequest::BlockHash(0); +//! let response_channel = read_handle.ready().await?.call(request); +//! let response = response_channel.await?; +//! assert_eq!( +//! response, +//! Response::BlockHash( +//! hex!("43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428") +//! ) +//! ); +//! +//! // This causes the writer thread on the +//! // other side of this handle to exit... +//! drop(write_handle); +//! // ...and this causes the reader thread-pool to exit. +//! drop(read_handle); +//! # Ok(()) } +//! ``` + +mod read; +pub use read::DatabaseReadHandle; + +mod write; +pub use write::DatabaseWriteHandle; + +mod free; +pub use free::init; + +// Internal type aliases for `service`. +mod types; + +#[cfg(test)] +mod tests; diff --git a/storage/database/src/service/read.rs b/storage/database/src/service/read.rs new file mode 100644 index 00000000..e53c7f88 --- /dev/null +++ b/storage/database/src/service/read.rs @@ -0,0 +1,493 @@ +//! Database reader thread-pool definitions and logic. + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + task::{Context, Poll}, +}; + +use futures::{channel::oneshot, ready}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use thread_local::ThreadLocal; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; +use tokio_util::sync::PollSemaphore; + +use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_types::{ + service::{ReadRequest, Response}, + ExtendedBlockHeader, OutputOnChain, +}; + +use crate::{ + config::ReaderThreads, + error::RuntimeError, + ops::{ + block::{get_block_extended_header_from_height, get_block_info}, + blockchain::{cumulative_generated_coins, top_block_height}, + key_image::key_image_exists, + output::id_to_output_on_chain, + }, + service::types::{ResponseReceiver, ResponseResult, ResponseSender}, + tables::{BlockHeights, BlockInfos, Tables}, + types::{Amount, AmountIndex, BlockHeight, KeyImage, PreRctOutputId}, + ConcreteEnv, DatabaseRo, Env, EnvInner, +}; + +//---------------------------------------------------------------------------------------------------- DatabaseReadHandle +/// Read handle to the database. +/// +/// This is cheaply [`Clone`]able handle that +/// allows `async`hronously reading from the database. +/// +/// Calling [`tower::Service::call`] with a [`DatabaseReadHandle`] & [`ReadRequest`] +/// will return an `async`hronous channel that can be `.await`ed upon +/// to receive the corresponding [`Response`]. +pub struct DatabaseReadHandle { + /// Handle to the custom `rayon` DB reader thread-pool. + /// + /// Requests are [`rayon::ThreadPool::spawn`]ed in this thread-pool, + /// and responses are returned via a channel we (the caller) provide. + pool: Arc<rayon::ThreadPool>, + + /// Counting semaphore asynchronous permit for database access. + /// Each [`tower::Service::poll_ready`] will acquire a permit + /// before actually sending a request to the `rayon` DB threadpool. + semaphore: PollSemaphore, + + /// An owned permit. + /// This will be set to [`Some`] in `poll_ready()` when we successfully acquire + /// the permit, and will be [`Option::take()`]n after `tower::Service::call()` is called. + /// + /// The actual permit will be dropped _after_ the rayon DB thread has finished + /// the request, i.e., after [`map_request()`] finishes. + permit: Option<OwnedSemaphorePermit>, + + /// Access to the database. + env: Arc<ConcreteEnv>, +} + +// `OwnedSemaphorePermit` does not implement `Clone`, +// so manually clone all elements, while keeping `permit` +// `None` across clones. +impl Clone for DatabaseReadHandle { + fn clone(&self) -> Self { + Self { + pool: Arc::clone(&self.pool), + semaphore: self.semaphore.clone(), + permit: None, + env: Arc::clone(&self.env), + } + } +} + +impl DatabaseReadHandle { + /// Initialize the `DatabaseReader` thread-pool backed by `rayon`. + /// + /// This spawns `N` amount of `DatabaseReader`'s + /// attached to `env` and returns a handle to the pool. + /// + /// Should be called _once_ per actual database. + #[cold] + #[inline(never)] // Only called once. + pub(super) fn init(env: &Arc<ConcreteEnv>, reader_threads: ReaderThreads) -> Self { + // How many reader threads to spawn? + let reader_count = reader_threads.as_threads().get(); + + // Spawn `rayon` reader threadpool. + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(reader_count) + .thread_name(|i| format!("cuprate_helper::service::read::DatabaseReader{i}")) + .build() + .unwrap(); + + // Create a semaphore with the same amount of + // permits as the amount of reader threads. + let semaphore = PollSemaphore::new(Arc::new(Semaphore::new(reader_count))); + + // Return a handle to the pool. + Self { + pool: Arc::new(pool), + semaphore, + permit: None, + env: Arc::clone(env), + } + } + + /// Access to the actual database environment. + /// + /// # ⚠️ Warning + /// This function gives you access to the actual + /// underlying database connected to by `self`. + /// + /// I.e. it allows you to read/write data _directly_ + /// instead of going through a request. + /// + /// Be warned that using the database directly + /// in this manner has not been tested. + #[inline] + pub const fn env(&self) -> &Arc<ConcreteEnv> { + &self.env + } +} + +impl tower::Service<ReadRequest> for DatabaseReadHandle { + type Response = Response; + type Error = RuntimeError; + type Future = ResponseReceiver; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + // Check if we already have a permit. + if self.permit.is_some() { + return Poll::Ready(Ok(())); + } + + // Acquire a permit before returning `Ready`. + let permit = + ready!(self.semaphore.poll_acquire(cx)).expect("this semaphore is never closed"); + + self.permit = Some(permit); + Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, request: ReadRequest) -> Self::Future { + let permit = self + .permit + .take() + .expect("poll_ready() should have acquire a permit before calling call()"); + + // Response channel we `.await` on. + let (response_sender, receiver) = oneshot::channel(); + + // Spawn the request in the rayon DB thread-pool. + // + // Note that this uses `self.pool` instead of `rayon::spawn` + // such that any `rayon` parallel code that runs within + // the passed closure uses the same `rayon` threadpool. + // + // INVARIANT: + // The below `DatabaseReader` function impl block relies on this behavior. + let env = Arc::clone(&self.env); + self.pool.spawn(move || { + let _permit: OwnedSemaphorePermit = permit; + map_request(&env, request, response_sender); + }); // drop(permit/env); + + InfallibleOneshotReceiver::from(receiver) + } +} + +//---------------------------------------------------------------------------------------------------- Request Mapping +// This function maps [`Request`]s to function calls +// executed by the rayon DB reader threadpool. + +/// Map [`Request`]'s to specific database handler functions. +/// +/// This is the main entrance into all `Request` handler functions. +/// The basic structure is: +/// 1. `Request` is mapped to a handler function +/// 2. Handler function is called +/// 3. [`Response`] is sent +fn map_request( + env: &ConcreteEnv, // Access to the database + request: ReadRequest, // The request we must fulfill + response_sender: ResponseSender, // The channel we must send the response back to +) { + use ReadRequest as R; + + /* SOMEDAY: pre-request handling, run some code for each request? */ + + let response = match request { + R::BlockExtendedHeader(block) => block_extended_header(env, block), + R::BlockHash(block) => block_hash(env, block), + R::BlockExtendedHeaderInRange(range) => block_extended_header_in_range(env, range), + R::ChainHeight => chain_height(env), + R::GeneratedCoins => generated_coins(env), + R::Outputs(map) => outputs(env, map), + R::NumberOutputsWithAmount(vec) => number_outputs_with_amount(env, vec), + R::CheckKIsNotSpent(set) => check_k_is_not_spent(env, set), + }; + + if let Err(e) = response_sender.send(response) { + // TODO: use tracing. + println!("database reader failed to send response: {e:?}"); + } + + /* SOMEDAY: post-request handling, run some code for each request? */ +} + +//---------------------------------------------------------------------------------------------------- Thread Local +/// Q: Why does this exist? +/// +/// A1: `heed`'s transactions and tables are not `Sync`, so we cannot use +/// them with rayon, however, we set a feature such that they are `Send`. +/// +/// A2: When sending to rayon, we want to ensure each read transaction +/// is only being used by 1 thread only to scale reads +/// +/// <https://github.com/Cuprate/cuprate/pull/113#discussion_r1576762346> +#[inline] +fn thread_local<T: Send>(env: &impl Env) -> ThreadLocal<T> { + ThreadLocal::with_capacity(env.config().reader_threads.as_threads().get()) +} + +/// Take in a `ThreadLocal<impl Tables>` and return an `&impl Tables + Send`. +/// +/// # Safety +/// See [`DatabaseRo`] docs. +/// +/// We are safely using `UnsafeSendable` in `service`'s reader thread-pool +/// as we are pairing our usage with `ThreadLocal` - only 1 thread +/// will ever access a transaction at a time. This is an INVARIANT. +/// +/// A `Mutex` was considered but: +/// - It is less performant +/// - It isn't technically needed for safety in our use-case +/// - It causes `DatabaseIter` function return issues as there is a `MutexGuard` object +/// +/// <https://github.com/Cuprate/cuprate/pull/113#discussion_r1581684698> +/// +/// # Notes +/// This is used for other backends as well instead of branching with `cfg_if`. +/// The other backends (as of current) are `Send + Sync` so this is fine. +/// <https://github.com/Cuprate/cuprate/pull/113#discussion_r1585618374> +macro_rules! get_tables { + ($env_inner:ident, $tx_ro:ident, $tables:ident) => {{ + $tables.get_or_try(|| { + #[allow(clippy::significant_drop_in_scrutinee)] + match $env_inner.open_tables($tx_ro) { + // SAFETY: see above macro doc comment. + Ok(tables) => Ok(unsafe { crate::unsafe_sendable::UnsafeSendable::new(tables) }), + Err(e) => Err(e), + } + }) + }}; +} + +//---------------------------------------------------------------------------------------------------- Handler functions +// These are the actual functions that do stuff according to the incoming [`Request`]. +// +// Each function name is a 1-1 mapping (from CamelCase -> snake_case) to +// the enum variant name, e.g: `BlockExtendedHeader` -> `block_extended_header`. +// +// Each function will return the [`Response`] that we +// should send back to the caller in [`map_request()`]. +// +// INVARIANT: +// These functions are called above in `tower::Service::call()` +// using a custom threadpool which means any call to `par_*()` functions +// will be using the custom rayon DB reader thread-pool, not the global one. +// +// All functions below assume that this is the case, such that +// `par_*()` functions will not block the _global_ rayon thread-pool. + +// FIXME: implement multi-transaction read atomicity. +// <https://github.com/Cuprate/cuprate/pull/113#discussion_r1576874589>. + +/// [`ReadRequest::BlockExtendedHeader`]. +#[inline] +fn block_extended_header(env: &ConcreteEnv, block_height: BlockHeight) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + let tables = env_inner.open_tables(&tx_ro)?; + + Ok(Response::BlockExtendedHeader( + get_block_extended_header_from_height(&block_height, &tables)?, + )) +} + +/// [`ReadRequest::BlockHash`]. +#[inline] +fn block_hash(env: &ConcreteEnv, block_height: BlockHeight) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + let table_block_infos = env_inner.open_db_ro::<BlockInfos>(&tx_ro)?; + + Ok(Response::BlockHash( + get_block_info(&block_height, &table_block_infos)?.block_hash, + )) +} + +/// [`ReadRequest::BlockExtendedHeaderInRange`]. +#[inline] +fn block_extended_header_in_range( + env: &ConcreteEnv, + range: std::ops::Range<BlockHeight>, +) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // Collect results using `rayon`. + let vec = range + .into_par_iter() + .map(|block_height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + get_block_extended_header_from_height(&block_height, tables) + }) + .collect::<Result<Vec<ExtendedBlockHeader>, RuntimeError>>()?; + + Ok(Response::BlockExtendedHeaderInRange(vec)) +} + +/// [`ReadRequest::ChainHeight`]. +#[inline] +fn chain_height(env: &ConcreteEnv) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + let table_block_heights = env_inner.open_db_ro::<BlockHeights>(&tx_ro)?; + let table_block_infos = env_inner.open_db_ro::<BlockInfos>(&tx_ro)?; + + let chain_height = crate::ops::blockchain::chain_height(&table_block_heights)?; + let block_hash = + get_block_info(&chain_height.saturating_sub(1), &table_block_infos)?.block_hash; + + Ok(Response::ChainHeight(chain_height, block_hash)) +} + +/// [`ReadRequest::GeneratedCoins`]. +#[inline] +fn generated_coins(env: &ConcreteEnv) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + let table_block_heights = env_inner.open_db_ro::<BlockHeights>(&tx_ro)?; + let table_block_infos = env_inner.open_db_ro::<BlockInfos>(&tx_ro)?; + + let top_height = top_block_height(&table_block_heights)?; + + Ok(Response::GeneratedCoins(cumulative_generated_coins( + &top_height, + &table_block_infos, + )?)) +} + +/// [`ReadRequest::Outputs`]. +#[inline] +fn outputs(env: &ConcreteEnv, outputs: HashMap<Amount, HashSet<AmountIndex>>) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // The 2nd mapping function. + // This is pulled out from the below `map()` for readability. + let inner_map = |amount, amount_index| -> Result<(AmountIndex, OutputOnChain), RuntimeError> { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + let id = PreRctOutputId { + amount, + amount_index, + }; + + let output_on_chain = id_to_output_on_chain(&id, tables)?; + + Ok((amount_index, output_on_chain)) + }; + + // Collect results using `rayon`. + let map = outputs + .into_par_iter() + .map(|(amount, amount_index_set)| { + Ok(( + amount, + amount_index_set + .into_par_iter() + .map(|amount_index| inner_map(amount, amount_index)) + .collect::<Result<HashMap<AmountIndex, OutputOnChain>, RuntimeError>>()?, + )) + }) + .collect::<Result<HashMap<Amount, HashMap<AmountIndex, OutputOnChain>>, RuntimeError>>()?; + + Ok(Response::Outputs(map)) +} + +/// [`ReadRequest::NumberOutputsWithAmount`]. +#[inline] +fn number_outputs_with_amount(env: &ConcreteEnv, amounts: Vec<Amount>) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // Cache the amount of RCT outputs once. + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + #[allow(clippy::cast_possible_truncation)] + let num_rct_outputs = { + let tx_ro = env_inner.tx_ro()?; + let tables = env_inner.open_tables(&tx_ro)?; + tables.rct_outputs().len()? as usize + }; + + // Collect results using `rayon`. + let map = amounts + .into_par_iter() + .map(|amount| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + if amount == 0 { + // v2 transactions. + Ok((amount, num_rct_outputs)) + } else { + // v1 transactions. + match tables.num_outputs().get(&amount) { + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + #[allow(clippy::cast_possible_truncation)] + Ok(count) => Ok((amount, count as usize)), + // If we get a request for an `amount` that doesn't exist, + // we return `0` instead of an error. + Err(RuntimeError::KeyNotFound) => Ok((amount, 0)), + Err(e) => Err(e), + } + } + }) + .collect::<Result<HashMap<Amount, usize>, RuntimeError>>()?; + + Ok(Response::NumberOutputsWithAmount(map)) +} + +/// [`ReadRequest::CheckKIsNotSpent`]. +#[inline] +fn check_k_is_not_spent(env: &ConcreteEnv, key_images: HashSet<KeyImage>) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // Key image check function. + let key_image_exists = |key_image| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + key_image_exists(&key_image, tables.key_images()) + }; + + // FIXME: + // Create/use `enum cuprate_types::Exist { Does, DoesNot }` + // or similar instead of `bool` for clarity. + // <https://github.com/Cuprate/cuprate/pull/113#discussion_r1581536526> + // + // Collect results using `rayon`. + match key_images + .into_par_iter() + .map(key_image_exists) + // If the result is either: + // `Ok(true)` => a key image was found, return early + // `Err` => an error was found, return early + // + // Else, `Ok(false)` will continue the iterator. + .find_any(|result| !matches!(result, Ok(false))) + { + None | Some(Ok(false)) => Ok(Response::CheckKIsNotSpent(true)), // Key image was NOT found. + Some(Ok(true)) => Ok(Response::CheckKIsNotSpent(false)), // Key image was found. + Some(Err(e)) => Err(e), // A database error occurred. + } +} diff --git a/storage/database/src/service/tests.rs b/storage/database/src/service/tests.rs new file mode 100644 index 00000000..77c10cdd --- /dev/null +++ b/storage/database/src/service/tests.rs @@ -0,0 +1,377 @@ +//! `crate::service` tests. +//! +//! This module contains general tests for the `service` implementation. + +// This is only imported on `#[cfg(test)]` in `mod.rs`. +#![allow(clippy::await_holding_lock, clippy::too_many_lines)] + +//---------------------------------------------------------------------------------------------------- Use +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use pretty_assertions::assert_eq; +use tower::{Service, ServiceExt}; + +use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; +use cuprate_types::{ + service::{ReadRequest, Response, WriteRequest}, + OutputOnChain, VerifiedBlockInformation, +}; + +use crate::{ + config::ConfigBuilder, + ops::{ + block::{get_block_extended_header_from_height, get_block_info}, + blockchain::chain_height, + output::id_to_output_on_chain, + }, + service::{init, DatabaseReadHandle, DatabaseWriteHandle}, + tables::{Tables, TablesIter}, + tests::AssertTableLen, + types::{Amount, AmountIndex, PreRctOutputId}, + ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, RuntimeError, +}; + +//---------------------------------------------------------------------------------------------------- Helper functions +/// Initialize the `service`. +fn init_service() -> ( + DatabaseReadHandle, + DatabaseWriteHandle, + Arc<ConcreteEnv>, + tempfile::TempDir, +) { + let tempdir = tempfile::tempdir().unwrap(); + let config = ConfigBuilder::new() + .db_directory(tempdir.path().into()) + .low_power() + .build(); + let (reader, writer) = init(config).unwrap(); + let env = reader.env().clone(); + (reader, writer, env, tempdir) +} + +/// This is the template used in the actual test functions below. +/// +/// - Send write request(s) +/// - Receive response(s) +/// - Assert proper tables were mutated +/// - Assert read requests lead to expected responses +#[allow(clippy::future_not_send)] // INVARIANT: tests are using a single threaded runtime +async fn test_template( + // Which block(s) to add? + block_fns: &[fn() -> &'static VerifiedBlockInformation], + // Total amount of generated coins after the block(s) have been added. + cumulative_generated_coins: u64, + // What are the table lengths be after the block(s) have been added? + assert_table_len: AssertTableLen, +) { + //----------------------------------------------------------------------- Write requests + let (reader, mut writer, env, _tempdir) = init_service(); + + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + // HACK: `add_block()` asserts blocks with non-sequential heights + // cannot be added, to get around this, manually edit the block height. + for (i, block_fn) in block_fns.iter().enumerate() { + let mut block = block_fn().clone(); + block.height = i as u64; + + // Request a block to be written, assert it was written. + let request = WriteRequest::WriteBlock(block); + let response_channel = writer.call(request); + let response = response_channel.await.unwrap(); + assert_eq!(response, Response::WriteBlockOk); + } + + //----------------------------------------------------------------------- Reset the transaction + drop(tables); + drop(tx_ro); + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + //----------------------------------------------------------------------- Assert all table lengths are correct + assert_table_len.assert(&tables); + + //----------------------------------------------------------------------- Read request prep + // Next few lines are just for preparing the expected responses, + // see further below for usage. + + let extended_block_header_0 = Ok(Response::BlockExtendedHeader( + get_block_extended_header_from_height(&0, &tables).unwrap(), + )); + + let extended_block_header_1 = if block_fns.len() > 1 { + Ok(Response::BlockExtendedHeader( + get_block_extended_header_from_height(&1, &tables).unwrap(), + )) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let block_hash_0 = Ok(Response::BlockHash( + get_block_info(&0, tables.block_infos()).unwrap().block_hash, + )); + + let block_hash_1 = if block_fns.len() > 1 { + Ok(Response::BlockHash( + get_block_info(&1, tables.block_infos()).unwrap().block_hash, + )) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let range_0_1 = Ok(Response::BlockExtendedHeaderInRange(vec![ + get_block_extended_header_from_height(&0, &tables).unwrap(), + ])); + + let range_0_2 = if block_fns.len() >= 2 { + Ok(Response::BlockExtendedHeaderInRange(vec![ + get_block_extended_header_from_height(&0, &tables).unwrap(), + get_block_extended_header_from_height(&1, &tables).unwrap(), + ])) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let chain_height = { + let height = chain_height(tables.block_heights()).unwrap(); + let block_info = get_block_info(&height.saturating_sub(1), tables.block_infos()).unwrap(); + Ok(Response::ChainHeight(height, block_info.block_hash)) + }; + + let cumulative_generated_coins = Ok(Response::GeneratedCoins(cumulative_generated_coins)); + + let num_req = tables + .outputs_iter() + .keys() + .unwrap() + .map(Result::unwrap) + .map(|key| key.amount) + .collect::<Vec<Amount>>(); + + let num_resp = Ok(Response::NumberOutputsWithAmount( + num_req + .iter() + .map(|amount| match tables.num_outputs().get(amount) { + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + #[allow(clippy::cast_possible_truncation)] + Ok(count) => (*amount, count as usize), + Err(RuntimeError::KeyNotFound) => (*amount, 0), + Err(e) => panic!("{e:?}"), + }) + .collect::<HashMap<Amount, usize>>(), + )); + + // Contains a fake non-spent key-image. + let ki_req = HashSet::from([[0; 32]]); + let ki_resp = Ok(Response::CheckKIsNotSpent(true)); + + //----------------------------------------------------------------------- Assert expected response + // Assert read requests lead to the expected responses. + for (request, expected_response) in [ + (ReadRequest::BlockExtendedHeader(0), extended_block_header_0), + (ReadRequest::BlockExtendedHeader(1), extended_block_header_1), + (ReadRequest::BlockHash(0), block_hash_0), + (ReadRequest::BlockHash(1), block_hash_1), + (ReadRequest::BlockExtendedHeaderInRange(0..1), range_0_1), + (ReadRequest::BlockExtendedHeaderInRange(0..2), range_0_2), + (ReadRequest::ChainHeight, chain_height), + (ReadRequest::GeneratedCoins, cumulative_generated_coins), + (ReadRequest::NumberOutputsWithAmount(num_req), num_resp), + (ReadRequest::CheckKIsNotSpent(ki_req), ki_resp), + ] { + let response = reader.clone().oneshot(request).await; + println!("response: {response:#?}, expected_response: {expected_response:#?}"); + match response { + Ok(resp) => assert_eq!(resp, expected_response.unwrap()), + Err(_) => assert!(matches!(response, _expected_response)), + } + } + + //----------------------------------------------------------------------- Key image checks + // Assert each key image we inserted comes back as "spent". + for key_image in tables.key_images_iter().keys().unwrap() { + let key_image = key_image.unwrap(); + let request = ReadRequest::CheckKIsNotSpent(HashSet::from([key_image])); + let response = reader.clone().oneshot(request).await; + println!("response: {response:#?}, key_image: {key_image:#?}"); + assert_eq!(response.unwrap(), Response::CheckKIsNotSpent(false)); + } + + //----------------------------------------------------------------------- Output checks + // Create the map of amounts and amount indices. + // + // FIXME: There's definitely a better way to map + // `Vec<PreRctOutputId>` -> `HashMap<u64, HashSet<u64>>` + let (map, output_count) = { + let mut ids = tables + .outputs_iter() + .keys() + .unwrap() + .map(Result::unwrap) + .collect::<Vec<PreRctOutputId>>(); + + ids.extend( + tables + .rct_outputs_iter() + .keys() + .unwrap() + .map(Result::unwrap) + .map(|amount_index| PreRctOutputId { + amount: 0, + amount_index, + }), + ); + + // Used later to compare the amount of Outputs + // returned in the Response is equal to the amount + // we asked for. + let output_count = ids.len(); + + let mut map = HashMap::<Amount, HashSet<AmountIndex>>::new(); + for id in ids { + map.entry(id.amount) + .and_modify(|set| { + set.insert(id.amount_index); + }) + .or_insert_with(|| HashSet::from([id.amount_index])); + } + + (map, output_count) + }; + + // Map `Output` -> `OutputOnChain` + // This is the expected output from the `Response`. + let outputs_on_chain = map + .iter() + .flat_map(|(amount, amount_index_set)| { + amount_index_set.iter().map(|amount_index| { + let id = PreRctOutputId { + amount: *amount, + amount_index: *amount_index, + }; + id_to_output_on_chain(&id, &tables).unwrap() + }) + }) + .collect::<Vec<OutputOnChain>>(); + + // Send a request for every output we inserted before. + let request = ReadRequest::Outputs(map.clone()); + let response = reader.clone().oneshot(request).await; + println!("Response::Outputs response: {response:#?}"); + let Ok(Response::Outputs(response)) = response else { + panic!("{response:#?}") + }; + + // Assert amount of `Amount`'s are the same. + assert_eq!(map.len(), response.len()); + + // Assert we get back the same map of + // `Amount`'s and `AmountIndex`'s. + let mut response_output_count = 0; + for (amount, output_map) in response { + let amount_index_set = map.get(&amount).unwrap(); + + for (amount_index, output) in output_map { + response_output_count += 1; + assert!(amount_index_set.contains(&amount_index)); + assert!(outputs_on_chain.contains(&output)); + } + } + + // Assert the amount of `Output`'s returned is as expected. + let table_output_len = tables.outputs().len().unwrap() + tables.rct_outputs().len().unwrap(); + assert_eq!(output_count as u64, table_output_len); + assert_eq!(output_count, response_output_count); +} + +//---------------------------------------------------------------------------------------------------- Tests +/// Simply `init()` the service and then drop it. +/// +/// If this test fails, something is very wrong. +#[test] +fn init_drop() { + let (_reader, _writer, _env, _tempdir) = init_service(); +} + +/// Assert write/read correctness of [`block_v1_tx2`]. +#[tokio::test] +async fn v1_tx2() { + test_template( + &[block_v1_tx2], + 14_535_350_982_449, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 65, + num_outputs: 41, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 111, + prunable_tx_blobs: 0, + rct_outputs: 0, + tx_blobs: 3, + tx_ids: 3, + tx_heights: 3, + tx_unlock_time: 1, + }, + ) + .await; +} + +/// Assert write/read correctness of [`block_v9_tx3`]. +#[tokio::test] +async fn v9_tx3() { + test_template( + &[block_v9_tx3], + 3_403_774_022_163, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 4, + num_outputs: 0, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 0, + prunable_tx_blobs: 0, + rct_outputs: 7, + tx_blobs: 4, + tx_ids: 4, + tx_heights: 4, + tx_unlock_time: 1, + }, + ) + .await; +} + +/// Assert write/read correctness of [`block_v16_tx0`]. +#[tokio::test] +async fn v16_tx0() { + test_template( + &[block_v16_tx0], + 600_000_000_000, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 0, + num_outputs: 0, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 0, + prunable_tx_blobs: 0, + rct_outputs: 1, + tx_blobs: 1, + tx_ids: 1, + tx_heights: 1, + tx_unlock_time: 1, + }, + ) + .await; +} diff --git a/storage/database/src/service/types.rs b/storage/database/src/service/types.rs new file mode 100644 index 00000000..265bf42c --- /dev/null +++ b/storage/database/src/service/types.rs @@ -0,0 +1,31 @@ +//! Database service type aliases. +//! +//! Only used internally for our `tower::Service` impls. + +//---------------------------------------------------------------------------------------------------- Use +use futures::channel::oneshot::Sender; + +use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_types::service::Response; + +use crate::error::RuntimeError; + +//---------------------------------------------------------------------------------------------------- Types +/// The actual type of the response. +/// +/// Either our [`Response`], or a database error occurred. +pub(super) type ResponseResult = Result<Response, RuntimeError>; + +/// The `Receiver` channel that receives the read response. +/// +/// This is owned by the caller (the reader/writer thread) +/// who `.await`'s for the response. +/// +/// The channel itself should never fail, +/// but the actual database operation might. +pub(super) type ResponseReceiver = InfallibleOneshotReceiver<ResponseResult>; + +/// The `Sender` channel for the response. +/// +/// The database reader/writer thread uses this to send the database result to the caller. +pub(super) type ResponseSender = Sender<ResponseResult>; diff --git a/storage/database/src/service/write.rs b/storage/database/src/service/write.rs new file mode 100644 index 00000000..d6747e97 --- /dev/null +++ b/storage/database/src/service/write.rs @@ -0,0 +1,245 @@ +//! Database writer thread definitions and logic. + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + sync::Arc, + task::{Context, Poll}, +}; + +use futures::channel::oneshot; + +use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_types::{ + service::{Response, WriteRequest}, + VerifiedBlockInformation, +}; + +use crate::{ + env::{Env, EnvInner}, + error::RuntimeError, + service::types::{ResponseReceiver, ResponseResult, ResponseSender}, + transaction::TxRw, + ConcreteEnv, +}; + +//---------------------------------------------------------------------------------------------------- Constants +/// Name of the writer thread. +const WRITER_THREAD_NAME: &str = concat!(module_path!(), "::DatabaseWriter"); + +//---------------------------------------------------------------------------------------------------- DatabaseWriteHandle +/// Write handle to the database. +/// +/// This is handle that allows `async`hronously writing to the database, +/// it is not [`Clone`]able as there is only ever 1 place within Cuprate +/// that writes. +/// +/// Calling [`tower::Service::call`] with a [`DatabaseWriteHandle`] & [`WriteRequest`] +/// will return an `async`hronous channel that can be `.await`ed upon +/// to receive the corresponding [`Response`]. +#[derive(Debug)] +pub struct DatabaseWriteHandle { + /// Sender channel to the database write thread-pool. + /// + /// We provide the response channel for the thread-pool. + pub(super) sender: crossbeam::channel::Sender<(WriteRequest, ResponseSender)>, +} + +impl DatabaseWriteHandle { + /// Initialize the single `DatabaseWriter` thread. + #[cold] + #[inline(never)] // Only called once. + pub(super) fn init(env: Arc<ConcreteEnv>) -> Self { + // Initialize `Request/Response` channels. + let (sender, receiver) = crossbeam::channel::unbounded(); + + // Spawn the writer. + std::thread::Builder::new() + .name(WRITER_THREAD_NAME.into()) + .spawn(move || { + let this = DatabaseWriter { receiver, env }; + DatabaseWriter::main(this); + }) + .unwrap(); + + Self { sender } + } +} + +impl tower::Service<WriteRequest> for DatabaseWriteHandle { + type Response = Response; + type Error = RuntimeError; + type Future = ResponseReceiver; + + #[inline] + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, request: WriteRequest) -> Self::Future { + // Response channel we `.await` on. + let (response_sender, receiver) = oneshot::channel(); + + // Send the write request. + self.sender.send((request, response_sender)).unwrap(); + + InfallibleOneshotReceiver::from(receiver) + } +} + +//---------------------------------------------------------------------------------------------------- DatabaseWriter +/// The single database writer thread. +pub(super) struct DatabaseWriter { + /// Receiver side of the database request channel. + /// + /// Any caller can send some requests to this channel. + /// They send them alongside another `Response` channel, + /// which we will eventually send to. + receiver: crossbeam::channel::Receiver<(WriteRequest, ResponseSender)>, + + /// Access to the database. + env: Arc<ConcreteEnv>, +} + +impl Drop for DatabaseWriter { + fn drop(&mut self) { + // TODO: log the writer thread has exited? + } +} + +impl DatabaseWriter { + /// The `DatabaseWriter`'s main function. + /// + /// The writer just loops in this function, handling requests forever + /// until the request channel is dropped or a panic occurs. + #[cold] + #[inline(never)] // Only called once. + fn main(self) { + // 1. Hang on request channel + // 2. Map request to some database function + // 3. Execute that function, get the result + // 4. Return the result via channel + 'main: loop { + let Ok((request, response_sender)) = self.receiver.recv() else { + // If this receive errors, it means that the channel is empty + // and disconnected, meaning the other side (all senders) have + // been dropped. This means "shutdown", and we return here to + // exit the thread. + // + // Since the channel is empty, it means we've also processed + // all requests. Since it is disconnected, it means future + // ones cannot come in. + return; + }; + + /// How many times should we retry handling the request on resize errors? + /// + /// This is 1 on automatically resizing databases, meaning there is only 1 iteration. + const REQUEST_RETRY_LIMIT: usize = if ConcreteEnv::MANUAL_RESIZE { 3 } else { 1 }; + + // Map [`Request`]'s to specific database functions. + // + // Both will: + // 1. Map the request to a function + // 2. Call the function + // 3. (manual resize only) If resize is needed, resize and retry + // 4. (manual resize only) Redo step {1, 2} + // 5. Send the function's `Result` back to the requester + // + // FIXME: there's probably a more elegant way + // to represent this retry logic with recursive + // functions instead of a loop. + 'retry: for retry in 0..REQUEST_RETRY_LIMIT { + // FIXME: will there be more than 1 write request? + // this won't have to be an enum. + let response = match &request { + WriteRequest::WriteBlock(block) => write_block(&self.env, block), + }; + + // If the database needs to resize, do so. + if ConcreteEnv::MANUAL_RESIZE && matches!(response, Err(RuntimeError::ResizeNeeded)) + { + // If this is the last iteration of the outer `for` loop and we + // encounter a resize error _again_, it means something is wrong. + assert_ne!( + retry, REQUEST_RETRY_LIMIT, + "database resize failed maximum of {REQUEST_RETRY_LIMIT} times" + ); + + // Resize the map, and retry the request handling loop. + // + // FIXME: + // We could pass in custom resizes to account for + // batches, i.e., we're about to add ~5GB of data, + // add that much instead of the default 1GB. + // <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L665-L695> + let old = self.env.current_map_size(); + let new = self.env.resize_map(None); + + // TODO: use tracing. + println!("resizing database memory map, old: {old}B, new: {new}B"); + + // Try handling the request again. + continue 'retry; + } + + // Automatically resizing databases should not be returning a resize error. + #[cfg(debug_assertions)] + if !ConcreteEnv::MANUAL_RESIZE { + assert!( + !matches!(response, Err(RuntimeError::ResizeNeeded)), + "auto-resizing database returned a ResizeNeeded error" + ); + } + + // Send the response back, whether if it's an `Ok` or `Err`. + if let Err(e) = response_sender.send(response) { + // TODO: use tracing. + println!("database writer failed to send response: {e:?}"); + } + + continue 'main; + } + + // Above retry loop should either: + // - continue to the next ['main] loop or... + // - ...retry until panic + unreachable!(); + } + } +} + +//---------------------------------------------------------------------------------------------------- Handler functions +// These are the actual functions that do stuff according to the incoming [`Request`]. +// +// Each function name is a 1-1 mapping (from CamelCase -> snake_case) to +// the enum variant name, e.g: `BlockExtendedHeader` -> `block_extended_header`. +// +// Each function will return the [`Response`] that we +// should send back to the caller in [`map_request()`]. + +/// [`WriteRequest::WriteBlock`]. +#[inline] +fn write_block(env: &ConcreteEnv, block: &VerifiedBlockInformation) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw()?; + + let result = { + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + crate::ops::block::add_block(block, &mut tables_mut) + }; + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(Response::WriteBlockOk) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} diff --git a/storage/database/src/storable.rs b/storage/database/src/storable.rs new file mode 100644 index 00000000..f259523f --- /dev/null +++ b/storage/database/src/storable.rs @@ -0,0 +1,347 @@ +//! (De)serialization for table keys & values. + +//---------------------------------------------------------------------------------------------------- Import +use std::{borrow::Borrow, fmt::Debug}; + +use bytemuck::Pod; +use bytes::Bytes; + +//---------------------------------------------------------------------------------------------------- Storable +/// A type that can be stored in the database. +/// +/// All keys and values in the database must be able +/// to be (de)serialized into/from raw bytes (`[u8]`). +/// +/// This trait represents types that can be **perfectly** +/// casted/represented as raw bytes. +/// +/// ## `bytemuck` +/// Any type that implements: +/// - [`bytemuck::Pod`] +/// - [`Debug`] +/// +/// will automatically implement [`Storable`]. +/// +/// This includes: +/// - Most primitive types +/// - All types in [`tables`](crate::tables) +/// +/// See [`StorableVec`] & [`StorableBytes`] for storing slices of `T: Storable`. +/// +/// ```rust +/// # use cuprate_database::*; +/// # use std::borrow::*; +/// let number: u64 = 0; +/// +/// // Into bytes. +/// let into = Storable::as_bytes(&number); +/// assert_eq!(into, &[0; 8]); +/// +/// // From bytes. +/// let from: u64 = Storable::from_bytes(&into); +/// assert_eq!(from, number); +/// ``` +/// +/// ## Invariants +/// No function in this trait is expected to panic. +/// +/// The byte conversions must execute flawlessly. +/// +/// ## Endianness +/// This trait doesn't currently care about endianness. +/// +/// Bytes are (de)serialized as-is, and `bytemuck` +/// types are architecture-dependant. +/// +/// Most likely, the bytes are little-endian, however +/// that cannot be relied upon when using this trait. +pub trait Storable: Debug { + /// Is this type fixed width in byte length? + /// + /// I.e., when converting `Self` to bytes, is it + /// represented with a fixed length array of bytes? + /// + /// # `Some` + /// This should be `Some(usize)` on types like: + /// - `u8` + /// - `u64` + /// - `i32` + /// + /// where the byte length is known. + /// + /// # `None` + /// This should be `None` on any variable-length type like: + /// - `str` + /// - `[u8]` + /// - `Vec<u8>` + /// + /// # Examples + /// ```rust + /// # use cuprate_database::*; + /// assert_eq!(<()>::BYTE_LENGTH, Some(0)); + /// assert_eq!(u8::BYTE_LENGTH, Some(1)); + /// assert_eq!(u16::BYTE_LENGTH, Some(2)); + /// assert_eq!(u32::BYTE_LENGTH, Some(4)); + /// assert_eq!(u64::BYTE_LENGTH, Some(8)); + /// assert_eq!(i8::BYTE_LENGTH, Some(1)); + /// assert_eq!(i16::BYTE_LENGTH, Some(2)); + /// assert_eq!(i32::BYTE_LENGTH, Some(4)); + /// assert_eq!(i64::BYTE_LENGTH, Some(8)); + /// assert_eq!(StorableVec::<u8>::BYTE_LENGTH, None); + /// assert_eq!(StorableVec::<u64>::BYTE_LENGTH, None); + /// ``` + const BYTE_LENGTH: Option<usize>; + + /// Return `self` in byte form. + fn as_bytes(&self) -> &[u8]; + + /// Create an owned [`Self`] from bytes. + /// + /// # Blanket implementation + /// The blanket implementation that covers all types used + /// by `cuprate_database` will simply bitwise copy `bytes` + /// into `Self`. + /// + /// The bytes do not have be correctly aligned. + fn from_bytes(bytes: &[u8]) -> Self; +} + +impl<T> Storable for T +where + Self: Pod + Debug, +{ + const BYTE_LENGTH: Option<usize> = Some(std::mem::size_of::<T>()); + + #[inline] + fn as_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } + + #[inline] + fn from_bytes(bytes: &[u8]) -> T { + bytemuck::pod_read_unaligned(bytes) + } +} + +//---------------------------------------------------------------------------------------------------- StorableVec +/// A [`Storable`] vector of `T: Storable`. +/// +/// This is a wrapper around `Vec<T> where T: Storable`. +/// +/// Slice types are owned both: +/// - when returned from the database +/// - in `put()` +/// +/// This is needed as `impl Storable for Vec<T>` runs into impl conflicts. +/// +/// # Example +/// ```rust +/// # use cuprate_database::*; +/// //---------------------------------------------------- u8 +/// let vec: StorableVec<u8> = StorableVec(vec![0,1]); +/// +/// // Into bytes. +/// let into = Storable::as_bytes(&vec); +/// assert_eq!(into, &[0,1]); +/// +/// // From bytes. +/// let from: StorableVec<u8> = Storable::from_bytes(&into); +/// assert_eq!(from, vec); +/// +/// //---------------------------------------------------- u64 +/// let vec: StorableVec<u64> = StorableVec(vec![0,1]); +/// +/// // Into bytes. +/// let into = Storable::as_bytes(&vec); +/// assert_eq!(into, &[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0]); +/// +/// // From bytes. +/// let from: StorableVec<u64> = Storable::from_bytes(&into); +/// assert_eq!(from, vec); +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::TransparentWrapper)] +#[repr(transparent)] +pub struct StorableVec<T>(pub Vec<T>); + +impl<T> Storable for StorableVec<T> +where + T: Pod + Debug, +{ + const BYTE_LENGTH: Option<usize> = None; + + /// Casts the inner `Vec<T>` directly as bytes. + #[inline] + fn as_bytes(&self) -> &[u8] { + bytemuck::must_cast_slice(&self.0) + } + + /// This always allocates a new `Vec<T>`, + /// casting `bytes` into a vector of type `T`. + #[inline] + fn from_bytes(bytes: &[u8]) -> Self { + Self(bytemuck::pod_collect_to_vec(bytes)) + } +} + +impl<T> std::ops::Deref for StorableVec<T> { + type Target = [T]; + #[inline] + fn deref(&self) -> &[T] { + &self.0 + } +} + +impl<T> Borrow<[T]> for StorableVec<T> { + #[inline] + fn borrow(&self) -> &[T] { + &self.0 + } +} + +//---------------------------------------------------------------------------------------------------- StorableBytes +/// A [`Storable`] version of [`Bytes`]. +/// +/// ```rust +/// # use cuprate_database::*; +/// # use bytes::Bytes; +/// let bytes: StorableBytes = StorableBytes(Bytes::from_static(&[0,1])); +/// +/// // Into bytes. +/// let into = Storable::as_bytes(&bytes); +/// assert_eq!(into, &[0,1]); +/// +/// // From bytes. +/// let from: StorableBytes = Storable::from_bytes(&into); +/// assert_eq!(from, bytes); +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct StorableBytes(pub Bytes); + +impl Storable for StorableBytes { + const BYTE_LENGTH: Option<usize> = None; + + #[inline] + fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// This always allocates a new `Bytes`. + #[inline] + fn from_bytes(bytes: &[u8]) -> Self { + Self(Bytes::copy_from_slice(bytes)) + } +} + +impl std::ops::Deref for StorableBytes { + type Target = [u8]; + #[inline] + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl Borrow<[u8]> for StorableBytes { + #[inline] + fn borrow(&self) -> &[u8] { + &self.0 + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; + + /// Serialize, deserialize, and compare that + /// the intermediate/end results are correct. + fn test_storable<const LEN: usize, T>( + // The primitive number function that + // converts the number into little endian bytes, + // e.g `u8::to_le_bytes`. + to_le_bytes: fn(T) -> [u8; LEN], + // A `Vec` of the numbers to test. + t: Vec<T>, + ) where + T: Storable + Debug + Copy + PartialEq, + { + for t in t { + let expected_bytes = to_le_bytes(t); + + println!("testing: {t:?}, expected_bytes: {expected_bytes:?}"); + + // (De)serialize. + let se: &[u8] = Storable::as_bytes(&t); + let de = <T as Storable>::from_bytes(se); + + println!("serialized: {se:?}, deserialized: {de:?}\n"); + + // Assert we wrote correct amount of bytes. + if T::BYTE_LENGTH.is_some() { + assert_eq!(se.len(), expected_bytes.len()); + } + // Assert the data is the same. + assert_eq!(de, t); + } + } + + /// Create all the float tests. + macro_rules! test_float { + ($( + $float:ident // The float type. + ),* $(,)?) => { + $( + #[test] + fn $float() { + test_storable( + $float::to_le_bytes, + vec![ + -1.0, + 0.0, + 1.0, + $float::MIN, + $float::MAX, + $float::INFINITY, + $float::NEG_INFINITY, + ], + ); + } + )* + }; + } + + test_float! { + f32, + f64, + } + + /// Create all the (un)signed number tests. + /// u8 -> u128, i8 -> i128. + macro_rules! test_unsigned { + ($( + $number:ident // The integer type. + ),* $(,)?) => { + $( + #[test] + fn $number() { + test_storable($number::to_le_bytes, vec![$number::MIN, 0, 1, $number::MAX]); + } + )* + }; + } + + test_unsigned! { + u8, + u16, + u32, + u64, + u128, + usize, + i8, + i16, + i32, + i64, + i128, + isize, + } +} diff --git a/storage/database/src/table.rs b/storage/database/src/table.rs new file mode 100644 index 00000000..966a9873 --- /dev/null +++ b/storage/database/src/table.rs @@ -0,0 +1,31 @@ +//! Database table abstraction; `trait Table`. + +//---------------------------------------------------------------------------------------------------- Import + +use crate::{key::Key, storable::Storable}; + +//---------------------------------------------------------------------------------------------------- Table +/// Database table metadata. +/// +/// Purely compile time information for database tables. +/// +/// ## Sealed +/// This trait is [`Sealed`](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed). +/// +/// It is only implemented on the types inside [`tables`][crate::tables]. +pub trait Table: crate::tables::private::Sealed + 'static { + /// Name of the database table. + const NAME: &'static str; + + /// Primary key type. + type Key: Key + 'static; + + /// Value type. + type Value: Storable + 'static; +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/tables.rs b/storage/database/src/tables.rs new file mode 100644 index 00000000..0056b0bd --- /dev/null +++ b/storage/database/src/tables.rs @@ -0,0 +1,476 @@ +//! Database tables. +//! +//! # Table marker structs +//! This module contains all the table definitions used by `cuprate_database`. +//! +//! The zero-sized structs here represents the table type; +//! they all are essentially marker types that implement [`Table`]. +//! +//! Table structs are `CamelCase`, and their static string +//! names used by the actual database backend are `snake_case`. +//! +//! For example: [`BlockBlobs`] -> `block_blobs`. +//! +//! # Traits +//! This module also contains a set of traits for +//! accessing _all_ tables defined here at once. +//! +//! For example, this is the object returned by [`EnvInner::open_tables`](crate::EnvInner::open_tables). + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + database::{DatabaseIter, DatabaseRo, DatabaseRw}, + table::Table, + types::{ + Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, + Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, + TxId, UnlockTime, + }, +}; + +//---------------------------------------------------------------------------------------------------- Sealed +/// Private module, should not be accessible outside this crate. +pub(super) mod private { + /// Private sealed trait. + /// + /// Cannot be implemented outside this crate. + pub trait Sealed {} +} + +//---------------------------------------------------------------------------------------------------- `trait Tables[Mut]` +/// Creates: +/// - `pub trait Tables` +/// - `pub trait TablesIter` +/// - `pub trait TablesMut` +/// - Blanket implementation for `(tuples, containing, all, open, database, tables, ...)` +/// +/// For why this exists, see: <https://github.com/Cuprate/cuprate/pull/102#pullrequestreview-1978348871>. +macro_rules! define_trait_tables { + ($( + // The `T: Table` type The index in a tuple + // | containing all tables + // v v + $table:ident => $index:literal + ),* $(,)?) => { paste::paste! { + /// Object containing all opened [`Table`]s in read-only mode. + /// + /// This is an encapsulated object that contains all + /// available [`Table`]'s in read-only mode. + /// + /// It is a `Sealed` trait and is only implemented on a + /// `(tuple, containing, all, table, types, ...)`. + /// + /// This is used to return a _single_ object from functions like + /// [`EnvInner::open_tables`](crate::EnvInner::open_tables) rather + /// than the tuple containing the tables itself. + /// + /// To replace `tuple.0` style indexing, `field_accessor_functions()` + /// are provided on this trait, which essentially map the object to + /// fields containing the particular database table, for example: + /// ```rust,ignore + /// let tables = open_tables(); + /// + /// // The accessor function `block_infos()` returns the field + /// // containing an open database table for `BlockInfos`. + /// let _ = tables.block_infos(); + /// ``` + /// + /// See also: + /// - [`TablesMut`] + /// - [`TablesIter`] + pub trait Tables: private::Sealed { + // This expands to creating `fn field_accessor_functions()` + // for each passed `$table` type. + // + // It is essentially a mapping to the field + // containing the proper opened database table. + // + // The function name of the function is + // the table type in `snake_case`, e.g., `block_info_v1s()`. + $( + /// Access an opened + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake>](&self) -> &impl DatabaseRo<$table>; + )* + + /// This returns `true` if all tables are empty. + /// + /// # Errors + /// This returns errors on regular database errors. + fn all_tables_empty(&self) -> Result<bool, $crate::error::RuntimeError>; + } + + /// Object containing all opened [`Table`]s in read + iter mode. + /// + /// This is the same as [`Tables`] but includes `_iter()` variants. + /// + /// Note that this trait is a supertrait of `Tables`, + /// as in it can use all of its functions as well. + /// + /// See [`Tables`] for documentation - this trait exists for the same reasons. + pub trait TablesIter: private::Sealed + Tables { + $( + /// Access an opened read-only + iterable + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake _iter>](&self) -> &(impl DatabaseRo<$table> + DatabaseIter<$table>); + )* + } + + /// Object containing all opened [`Table`]s in write mode. + /// + /// This is the same as [`Tables`] but for mutable accesses. + /// + /// Note that this trait is a supertrait of `Tables`, + /// as in it can use all of its functions as well. + /// + /// See [`Tables`] for documentation - this trait exists for the same reasons. + pub trait TablesMut: private::Sealed + Tables { + $( + /// Access an opened + #[doc = concat!("[`", stringify!($table), "`]")] + /// database. + fn [<$table:snake _mut>](&mut self) -> &mut impl DatabaseRw<$table>; + )* + } + + // Implement `Sealed` for all table types. + impl<$([<$table:upper>]),*> private::Sealed for ($([<$table:upper>]),*) {} + + // This creates a blanket-implementation for + // `(tuple, containing, all, table, types)`. + // + // There is a generic defined here _for each_ `$table` input. + // Specifically, the generic letters are just the table types in UPPERCASE. + // Concretely, this expands to something like: + // ```rust + // impl<BLOCKINFOSV1S, BLOCKINFOSV2S, BLOCKINFOSV3S, [...]> + // ``` + impl<$([<$table:upper>]),*> Tables + // We are implementing `Tables` on a tuple that + // contains all those generics specified, i.e., + // a tuple containing all open table types. + // + // Concretely, this expands to something like: + // ```rust + // (BLOCKINFOSV1S, BLOCKINFOSV2S, BLOCKINFOSV3S, [...]) + // ``` + // which is just a tuple of the generics defined above. + for ($([<$table:upper>]),*) + where + // This expands to a where bound that asserts each element + // in the tuple implements some database table type. + // + // Concretely, this expands to something like: + // ```rust + // BLOCKINFOSV1S: DatabaseRo<BlockInfoV1s> + DatabaseIter<BlockInfoV1s>, + // BLOCKINFOSV2S: DatabaseRo<BlockInfoV2s> + DatabaseIter<BlockInfoV2s>, + // [...] + // ``` + $( + [<$table:upper>]: DatabaseRo<$table>, + )* + { + $( + // The function name of the accessor function is + // the table type in `snake_case`, e.g., `block_info_v1s()`. + #[inline] + fn [<$table:snake>](&self) -> &impl DatabaseRo<$table> { + // The index of the database table in + // the tuple implements the table trait. + &self.$index + } + )* + + fn all_tables_empty(&self) -> Result<bool, $crate::error::RuntimeError> { + $( + if !DatabaseRo::is_empty(&self.$index)? { + return Ok(false); + } + )* + Ok(true) + } + } + + // This is the same as the above + // `Tables`, but for `TablesIter`. + impl<$([<$table:upper>]),*> TablesIter + for ($([<$table:upper>]),*) + where + $( + [<$table:upper>]: DatabaseRo<$table> + DatabaseIter<$table>, + )* + { + $( + // The function name of the accessor function is + // the table type in `snake_case` + `_iter`, e.g., `block_info_v1s_iter()`. + #[inline] + fn [<$table:snake _iter>](&self) -> &(impl DatabaseRo<$table> + DatabaseIter<$table>) { + &self.$index + } + )* + } + + // This is the same as the above + // `Tables`, but for `TablesMut`. + impl<$([<$table:upper>]),*> TablesMut + for ($([<$table:upper>]),*) + where + $( + [<$table:upper>]: DatabaseRw<$table>, + )* + { + $( + // The function name of the mutable accessor function is + // the table type in `snake_case` + `_mut`, e.g., `block_info_v1s_mut()`. + #[inline] + fn [<$table:snake _mut>](&mut self) -> &mut impl DatabaseRw<$table> { + &mut self.$index + } + )* + } + }}; +} + +// Input format: $table_type => $index +// +// The $index: +// - Simply increments by 1 for each table +// - Must be 0.. +// - Must end at the total amount of table types - 1 +// +// Compile errors will occur if these aren't satisfied. +// +// $index is just the `tuple.$index`, as the above [`define_trait_tables`] +// macro has a blanket impl for `(all, table, types, ...)` and we must map +// each type to a tuple index explicitly. +// +// FIXME: there's definitely an automatic way to this :) +define_trait_tables! { + BlockInfos => 0, + BlockBlobs => 1, + BlockHeights => 2, + KeyImages => 3, + NumOutputs => 4, + PrunedTxBlobs => 5, + PrunableHashes => 6, + Outputs => 7, + PrunableTxBlobs => 8, + RctOutputs => 9, + TxBlobs => 10, + TxIds => 11, + TxHeights => 12, + TxOutputs => 13, + TxUnlockTime => 14, +} + +//---------------------------------------------------------------------------------------------------- Table function macro +/// `crate`-private macro for callings functions on all tables. +/// +/// This calls the function `$fn` with the optional +/// arguments `$args` on all tables - returning early +/// (within whatever scope this is called) if any +/// of the function calls error. +/// +/// Else, it evaluates to an `Ok((tuple, of, all, table, types, ...))`, +/// i.e., an `impl Table[Mut]` wrapped in `Ok`. +macro_rules! call_fn_on_all_tables_or_early_return { + ( + $($fn:ident $(::)?)* + ( + $($arg:ident),* $(,)? + ) + ) => {{ + Ok(( + $($fn ::)*<$crate::tables::BlockInfos>($($arg),*)?, + $($fn ::)*<$crate::tables::BlockBlobs>($($arg),*)?, + $($fn ::)*<$crate::tables::BlockHeights>($($arg),*)?, + $($fn ::)*<$crate::tables::KeyImages>($($arg),*)?, + $($fn ::)*<$crate::tables::NumOutputs>($($arg),*)?, + $($fn ::)*<$crate::tables::PrunedTxBlobs>($($arg),*)?, + $($fn ::)*<$crate::tables::PrunableHashes>($($arg),*)?, + $($fn ::)*<$crate::tables::Outputs>($($arg),*)?, + $($fn ::)*<$crate::tables::PrunableTxBlobs>($($arg),*)?, + $($fn ::)*<$crate::tables::RctOutputs>($($arg),*)?, + $($fn ::)*<$crate::tables::TxBlobs>($($arg),*)?, + $($fn ::)*<$crate::tables::TxIds>($($arg),*)?, + $($fn ::)*<$crate::tables::TxHeights>($($arg),*)?, + $($fn ::)*<$crate::tables::TxOutputs>($($arg),*)?, + $($fn ::)*<$crate::tables::TxUnlockTime>($($arg),*)?, + )) + }}; +} +pub(crate) use call_fn_on_all_tables_or_early_return; + +//---------------------------------------------------------------------------------------------------- Table macro +/// Create all tables, should be used _once_. +/// +/// Generating this macro once and using `$()*` is probably +/// faster for compile times than calling the macro _per_ table. +/// +/// All tables are zero-sized table structs, and implement the `Table` trait. +/// +/// Table structs are automatically `CamelCase`, +/// and their static string names are automatically `snake_case`. +macro_rules! tables { + ( + $( + $(#[$attr:meta])* // Documentation and any `derive`'s. + $table:ident, // The table name + doubles as the table struct name. + $key:ty => // Key type. + $value:ty // Value type. + ),* $(,)? + ) => { + paste::paste! { $( + // Table struct. + $(#[$attr])* + // The below test show the `snake_case` table name in cargo docs. + #[doc = concat!("- Key: [`", stringify!($key), "`]")] + #[doc = concat!("- Value: [`", stringify!($value), "`]")] + /// + /// ## Table Name + /// ```rust + /// # use cuprate_database::{*,tables::*}; + #[doc = concat!( + "assert_eq!(", + stringify!([<$table:camel>]), + "::NAME, \"", + stringify!([<$table:snake>]), + "\");", + )] + /// ``` + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] + pub struct [<$table:camel>]; + + // Implement the `Sealed` in this file. + // Required by `Table`. + impl private::Sealed for [<$table:camel>] {} + + // Table trait impl. + impl Table for [<$table:camel>] { + const NAME: &'static str = stringify!([<$table:snake>]); + type Key = $key; + type Value = $value; + } + )* } + }; +} + +//---------------------------------------------------------------------------------------------------- Tables +// Notes: +// - Keep this sorted A-Z (by table name) +// - Tables are defined in plural to avoid name conflicts with types +// - If adding/changing a table also edit: +// a) the tests in `src/backend/tests.rs` +// b) `Env::open` to make sure it creates the table (for all backends) +// c) `call_fn_on_all_tables_or_early_return!()` macro defined in this file +tables! { + /// Serialized block blobs (bytes). + /// + /// Contains the serialized version of all blocks. + BlockBlobs, + BlockHeight => BlockBlob, + + /// Block heights. + /// + /// Contains the height of all blocks. + BlockHeights, + BlockHash => BlockHeight, + + /// Block information. + /// + /// Contains metadata of all blocks. + BlockInfos, + BlockHeight => BlockInfo, + + /// Set of key images. + /// + /// Contains all the key images known to be spent. + /// + /// This table has `()` as the value type, as in, + /// it is a set of key images. + KeyImages, + KeyImage => (), + + /// Maps an output's amount to the number of outputs with that amount. + /// + /// For example, if there are 5 outputs with `amount = 123` + /// then calling `get(123)` on this table will return 5. + NumOutputs, + Amount => u64, + + /// Pre-RCT output data. + Outputs, + PreRctOutputId => Output, + + /// Pruned transaction blobs (bytes). + /// + /// Contains the pruned portion of serialized transaction data. + PrunedTxBlobs, + TxId => PrunedBlob, + + /// Prunable transaction blobs (bytes). + /// + /// Contains the prunable portion of serialized transaction data. + // SOMEDAY: impl when `monero-serai` supports pruning + PrunableTxBlobs, + TxId => PrunableBlob, + + /// Prunable transaction hashes. + /// + /// Contains the prunable portion of transaction hashes. + // SOMEDAY: impl when `monero-serai` supports pruning + PrunableHashes, + TxId => PrunableHash, + + // SOMEDAY: impl a properties table: + // - db version + // - pruning seed + // Properties, + // StorableString => StorableVec, + + /// RCT output data. + RctOutputs, + AmountIndex => RctOutput, + + /// Transaction blobs (bytes). + /// + /// Contains the serialized version of all transactions. + // SOMEDAY: remove when `monero-serai` supports pruning + TxBlobs, + TxId => TxBlob, + + /// Transaction indices. + /// + /// Contains the indices all transactions. + TxIds, + TxHash => TxId, + + /// Transaction heights. + /// + /// Contains the block height associated with all transactions. + TxHeights, + TxId => BlockHeight, + + /// Transaction outputs. + /// + /// Contains the list of `AmountIndex`'s of the + /// outputs associated with all transactions. + TxOutputs, + TxId => AmountIndices, + + /// Transaction unlock time. + /// + /// Contains the unlock time of transactions IF they have one. + /// Transactions without unlock times will not exist in this table. + TxUnlockTime, + TxId => UnlockTime, +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/tests.rs b/storage/database/src/tests.rs new file mode 100644 index 00000000..ba5e8550 --- /dev/null +++ b/storage/database/src/tests.rs @@ -0,0 +1,85 @@ +//! Utilities for `cuprate_database` testing. +//! +//! These types/fn's are only: +//! - enabled on #[cfg(test)] +//! - only used internally + +//---------------------------------------------------------------------------------------------------- Import +use std::fmt::Debug; + +use pretty_assertions::assert_eq; + +use crate::{config::ConfigBuilder, tables::Tables, ConcreteEnv, DatabaseRo, Env, EnvInner}; + +//---------------------------------------------------------------------------------------------------- Struct +/// Named struct to assert the length of all tables. +/// +/// This is a struct with fields instead of a function +/// so that callers can name arguments, otherwise the call-site +/// is a little confusing, i.e. `assert_table_len(0, 25, 1, 123)`. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct AssertTableLen { + pub(crate) block_infos: u64, + pub(crate) block_blobs: u64, + pub(crate) block_heights: u64, + pub(crate) key_images: u64, + pub(crate) num_outputs: u64, + pub(crate) pruned_tx_blobs: u64, + pub(crate) prunable_hashes: u64, + pub(crate) outputs: u64, + pub(crate) prunable_tx_blobs: u64, + pub(crate) rct_outputs: u64, + pub(crate) tx_blobs: u64, + pub(crate) tx_ids: u64, + pub(crate) tx_heights: u64, + pub(crate) tx_unlock_time: u64, +} + +impl AssertTableLen { + /// Assert the length of all tables. + pub(crate) fn assert(self, tables: &impl Tables) { + let other = Self { + block_infos: tables.block_infos().len().unwrap(), + block_blobs: tables.block_blobs().len().unwrap(), + block_heights: tables.block_heights().len().unwrap(), + key_images: tables.key_images().len().unwrap(), + num_outputs: tables.num_outputs().len().unwrap(), + pruned_tx_blobs: tables.pruned_tx_blobs().len().unwrap(), + prunable_hashes: tables.prunable_hashes().len().unwrap(), + outputs: tables.outputs().len().unwrap(), + prunable_tx_blobs: tables.prunable_tx_blobs().len().unwrap(), + rct_outputs: tables.rct_outputs().len().unwrap(), + tx_blobs: tables.tx_blobs().len().unwrap(), + tx_ids: tables.tx_ids().len().unwrap(), + tx_heights: tables.tx_heights().len().unwrap(), + tx_unlock_time: tables.tx_unlock_time().len().unwrap(), + }; + + assert_eq!(self, other); + } +} + +//---------------------------------------------------------------------------------------------------- fn +/// Create an `Env` in a temporarily directory. +/// The directory is automatically removed after the `TempDir` is dropped. +/// +/// FIXME: changing this to `-> impl Env` causes lifetime errors... +pub(crate) fn tmp_concrete_env() -> (ConcreteEnv, tempfile::TempDir) { + let tempdir = tempfile::tempdir().unwrap(); + let config = ConfigBuilder::new() + .db_directory(tempdir.path().into()) + .low_power() + .build(); + let env = ConcreteEnv::open(config).unwrap(); + + (env, tempdir) +} + +/// Assert all the tables in the environment are empty. +pub(crate) fn assert_all_tables_are_empty(env: &ConcreteEnv) { + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + assert!(tables.all_tables_empty().unwrap()); + assert_eq!(crate::ops::tx::get_num_tx(tables.tx_ids()).unwrap(), 0); +} diff --git a/storage/database/src/transaction.rs b/storage/database/src/transaction.rs new file mode 100644 index 00000000..e4c310a0 --- /dev/null +++ b/storage/database/src/transaction.rs @@ -0,0 +1,43 @@ +//! Database transaction abstraction; `trait TxRo`, `trait TxRw`. + +//---------------------------------------------------------------------------------------------------- Import +use crate::error::RuntimeError; + +//---------------------------------------------------------------------------------------------------- TxRo +/// Read-only database transaction. +/// +/// Returned from [`EnvInner::tx_ro`](crate::EnvInner::tx_ro). +/// +/// # Commit +/// It's recommended but may not be necessary to call [`TxRo::commit`] in certain cases: +/// - <https://docs.rs/heed/0.20.0-alpha.9/heed/struct.RoTxn.html#method.commit> +pub trait TxRo<'env> { + /// Commit the read-only transaction. + /// + /// # Errors + /// This operation will always return `Ok(())` with the `redb` backend. + fn commit(self) -> Result<(), RuntimeError>; +} + +//---------------------------------------------------------------------------------------------------- TxRw +/// Read/write database transaction. +/// +/// Returned from [`EnvInner::tx_rw`](crate::EnvInner::tx_rw). +pub trait TxRw<'env> { + /// Commit the read/write transaction. + /// + /// Note that this doesn't necessarily sync the database caches to disk. + /// + /// # Errors + /// This operation will always return `Ok(())` with the `redb` backend. + /// + /// If `Env::MANUAL_RESIZE == true`, + /// [`RuntimeError::ResizeNeeded`] may be returned. + fn commit(self) -> Result<(), RuntimeError>; + + /// Abort the transaction, erasing any writes that have occurred. + /// + /// # Errors + /// This operation will always return `Ok(())` with the `heed` backend. + fn abort(self) -> Result<(), RuntimeError>; +} diff --git a/storage/database/src/types.rs b/storage/database/src/types.rs new file mode 100644 index 00000000..5d89d4c4 --- /dev/null +++ b/storage/database/src/types.rs @@ -0,0 +1,324 @@ +//! Database [table](crate::tables) types. +//! +//! This module contains all types used by the database tables, +//! and aliases for common Monero-related types that use the +//! same underlying primitive type. +//! +//! <!-- FIXME: Add schema here or a link to it when complete --> + +/* + * <============================================> VERY BIG SCARY SAFETY MESSAGE <============================================> + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * + * + * + * We use `bytemuck` to (de)serialize data types in the database. + * We are SAFELY casting bytes, but to do so, we must uphold some invariants. + * When editing this file, there is only 1 commandment that MUST be followed: + * + * 1. Thou shall only utilize `bytemuck`'s derive macros + * + * The derive macros will fail at COMPILE time if something is incorrect. + * <https://docs.rs/bytemuck/latest/bytemuck/derive.Pod.html> + * If you submit a PR that breaks this I will come and find you. + * + * + * + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE --- DO NOT IGNORE + * <============================================> VERY BIG SCARY SAFETY MESSAGE <============================================> + */ +// actually i still don't trust you. no unsafe. +#![forbid(unsafe_code)] // if you remove this line i will steal your monero + +//---------------------------------------------------------------------------------------------------- Import +use bytemuck::{Pod, Zeroable}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::storable::StorableVec; + +//---------------------------------------------------------------------------------------------------- Aliases +// These type aliases exist as many Monero-related types are the exact same. +// For clarity, they're given type aliases as to not confuse them. + +/// An output's amount. +pub type Amount = u64; + +/// The index of an [`Amount`] in a list of duplicate `Amount`s. +pub type AmountIndex = u64; + +/// A list of [`AmountIndex`]s. +pub type AmountIndices = StorableVec<AmountIndex>; + +/// A serialized block. +pub type BlockBlob = StorableVec<u8>; + +/// A block's hash. +pub type BlockHash = [u8; 32]; + +/// A block's height. +pub type BlockHeight = u64; + +/// A key image. +pub type KeyImage = [u8; 32]; + +/// Pruned serialized bytes. +pub type PrunedBlob = StorableVec<u8>; + +/// A prunable serialized bytes. +pub type PrunableBlob = StorableVec<u8>; + +/// A prunable hash. +pub type PrunableHash = [u8; 32]; + +/// A serialized transaction. +pub type TxBlob = StorableVec<u8>; + +/// A transaction's global index, or ID. +pub type TxId = u64; + +/// A transaction's hash. +pub type TxHash = [u8; 32]; + +/// The unlock time value of an output. +pub type UnlockTime = u64; + +//---------------------------------------------------------------------------------------------------- BlockInfoV1 +/// A identifier for a pre-RCT [`Output`]. +/// +/// This can also serve as an identifier for [`RctOutput`]'s +/// when [`PreRctOutputId::amount`] is set to `0`, although, +/// in that case, only [`AmountIndex`] needs to be known. +/// +/// This is the key to the [`Outputs`](crate::tables::Outputs) table. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_database::{*, types::*}; +/// // Assert Storable is correct. +/// let a = PreRctOutputId { +/// amount: 1, +/// amount_index: 123, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: PreRctOutputId = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_database::types::*; +/// # use std::mem::*; +/// assert_eq!(size_of::<PreRctOutputId>(), 16); +/// assert_eq!(align_of::<PreRctOutputId>(), 8); +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct PreRctOutputId { + /// Amount of the output. + /// + /// This should be `0` if the output is an [`RctOutput`]. + pub amount: Amount, + /// The index of the output with the same `amount`. + /// + /// In the case of [`Output`]'s, this is the index of the list + /// of outputs with the same clear amount. + /// + /// In the case of [`RctOutput`]'s, this is the + /// global index of _all_ `RctOutput`s + pub amount_index: AmountIndex, +} + +//---------------------------------------------------------------------------------------------------- BlockInfoV3 +/// Block information. +/// +/// This is the value in the [`BlockInfos`](crate::tables::BlockInfos) table. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_database::{*, types::*}; +/// // Assert Storable is correct. +/// let a = BlockInfo { +/// timestamp: 1, +/// cumulative_generated_coins: 123, +/// weight: 321, +/// cumulative_difficulty_low: 112, +/// cumulative_difficulty_high: 112, +/// block_hash: [54; 32], +/// cumulative_rct_outs: 2389, +/// long_term_weight: 2389, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: BlockInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_database::types::*; +/// # use std::mem::*; +/// assert_eq!(size_of::<BlockInfo>(), 88); +/// assert_eq!(align_of::<BlockInfo>(), 8); +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct BlockInfo { + /// The UNIX time at which the block was mined. + pub timestamp: u64, + /// The total amount of coins mined in all blocks so far, including this block's. + pub cumulative_generated_coins: u64, + /// The adjusted block size, in bytes. + /// + /// See [`block_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#blocks-weight). + pub weight: u64, + /// Least-significant 64 bits of the 128-bit cumulative difficulty. + pub cumulative_difficulty_low: u64, + /// Most-significant 64 bits of the 128-bit cumulative difficulty. + pub cumulative_difficulty_high: u64, + /// The block's hash. + pub block_hash: [u8; 32], + /// The total amount of RCT outputs so far, including this block's. + pub cumulative_rct_outs: u64, + /// The long term block weight, based on the median weight of the preceding `100_000` blocks. + /// + /// See [`long_term_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#long-term-block-weight). + pub long_term_weight: u64, +} + +//---------------------------------------------------------------------------------------------------- OutputFlags +bitflags::bitflags! { + /// Bit flags for [`Output`]s and [`RctOutput`]s, + /// + /// Currently only the first bit is used and, if set, + /// it means this output has a non-zero unlock time. + /// + /// ```rust + /// # use std::borrow::*; + /// # use cuprate_database::{*, types::*}; + /// // Assert Storable is correct. + /// let a = OutputFlags::NON_ZERO_UNLOCK_TIME; + /// let b = Storable::as_bytes(&a); + /// let c: OutputFlags = Storable::from_bytes(b); + /// assert_eq!(a, c); + /// ``` + /// + /// # Size & Alignment + /// ```rust + /// # use cuprate_database::types::*; + /// # use std::mem::*; + /// assert_eq!(size_of::<OutputFlags>(), 4); + /// assert_eq!(align_of::<OutputFlags>(), 4); + /// ``` + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] + #[repr(transparent)] + pub struct OutputFlags: u32 { + /// This output has a non-zero unlock time. + const NON_ZERO_UNLOCK_TIME = 0b0000_0001; + } +} + +//---------------------------------------------------------------------------------------------------- Output +/// A pre-RCT (v1) output's data. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_database::{*, types::*}; +/// // Assert Storable is correct. +/// let a = Output { +/// key: [1; 32], +/// height: 1, +/// output_flags: OutputFlags::empty(), +/// tx_idx: 3, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: Output = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_database::types::*; +/// # use std::mem::*; +/// assert_eq!(size_of::<Output>(), 48); +/// assert_eq!(align_of::<Output>(), 8); +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct Output { + /// The public key of the output. + pub key: [u8; 32], + /// The block height this output belongs to. + // PERF: We could get this from the tx_idx with the `TxHeights` + // table but that would require another look up per out. + pub height: u32, + /// Bit flags for this output. + pub output_flags: OutputFlags, + /// The index of the transaction this output belongs to. + pub tx_idx: u64, +} + +//---------------------------------------------------------------------------------------------------- RctOutput +/// An RCT (v2+) output's data. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_database::{*, types::*}; +/// // Assert Storable is correct. +/// let a = RctOutput { +/// key: [1; 32], +/// height: 1, +/// output_flags: OutputFlags::empty(), +/// tx_idx: 3, +/// commitment: [3; 32], +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: RctOutput = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_database::types::*; +/// # use std::mem::*; +/// assert_eq!(size_of::<RctOutput>(), 80); +/// assert_eq!(align_of::<RctOutput>(), 8); +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct RctOutput { + /// The public key of the output. + pub key: [u8; 32], + /// The block height this output belongs to. + // PERF: We could get this from the tx_idx with the `TxHeights` + // table but that would require another look up per out. + pub height: u32, + /// Bit flags for this output, currently only the first bit is used and, if set, it means this output has a non-zero unlock time. + pub output_flags: OutputFlags, + /// The index of the transaction this output belongs to. + pub tx_idx: u64, + /// The amount commitment of this output. + pub commitment: [u8; 32], +} +// TODO: local_index? + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/storage/database/src/unsafe_sendable.rs b/storage/database/src/unsafe_sendable.rs new file mode 100644 index 00000000..94472933 --- /dev/null +++ b/storage/database/src/unsafe_sendable.rs @@ -0,0 +1,85 @@ +//! Wrapper type for partially-`unsafe` usage of `T: !Send`. + +//---------------------------------------------------------------------------------------------------- Import +use std::{ + borrow::Borrow, + ops::{Deref, DerefMut}, +}; + +use bytemuck::TransparentWrapper; + +//---------------------------------------------------------------------------------------------------- Aliases +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, TransparentWrapper)] +#[repr(transparent)] +/// A wrapper type that `unsafe`ly implements `Send` for any `T`. +/// +/// This is a marker/wrapper type that allows wrapping +/// any type `T` such that it implements `Send`. +/// +/// This is to be used when `T` is `Send`, but only in certain +/// situations not provable to the compiler, or is otherwise a +/// a pain to prove and/or less efficient. +/// +/// It is up to the users of this type to ensure their +/// usage of `UnsafeSendable` are actually safe. +/// +/// Notably, `heed`'s table type uses this inside `service`. +pub(crate) struct UnsafeSendable<T>(T); + +#[allow(clippy::non_send_fields_in_send_ty)] +// SAFETY: Users ensure that their usage of this type is safe. +unsafe impl<T> Send for UnsafeSendable<T> {} + +impl<T> UnsafeSendable<T> { + /// Create a new [`UnsafeSendable`]. + /// + /// # Safety + /// By constructing this type, you must ensure the usage + /// of the resulting `Self` is follows all the [`Send`] rules. + pub(crate) const unsafe fn new(t: T) -> Self { + Self(t) + } + + /// Extract the inner `T`. + #[allow(dead_code)] + pub(crate) fn into_inner(self) -> T { + self.0 + } +} + +impl<T> Borrow<T> for UnsafeSendable<T> { + fn borrow(&self) -> &T { + &self.0 + } +} + +impl<T> AsRef<T> for UnsafeSendable<T> { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl<T> AsMut<T> for UnsafeSendable<T> { + fn as_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl<T> Deref for UnsafeSendable<T> { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<T> DerefMut for UnsafeSendable<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +}