Skip to content

Commit

Permalink
Move storage traits implementation to the fuel-core-storage crate (F…
Browse files Browse the repository at this point in the history
…uelLabs#1576)

## Overview

Closes FuelLabs#1548 
Closes FuelLabs#430

The change moves the implementation of the storage traits for required
tables from `fuel-core` to `fuel-core-storage` crate. The change also
adds a more flexible configuration of the encoding/decoding per the
table and allows the implementation of specific behaviors for the table
in a much easier way. It unifies the encoding between database, SMTs,
and iteration, preventing mismatching bytes representation on the Rust
type system level. Plus, it increases the re-usage of the code by
applying the same structure to other tables.

It is a breaking PR because it changes database encoding/decoding for
some tables.

### StructuredStorage

The change adds a new type `StructuredStorage`. It is a wrapper around
the key-value storage that implements the storage
traits(`StorageInspect`, `StorageMutate`, `StorageRead`, etc) for the
tables with structure. This structure works in tandem with the
`TableWithStructure` trait. The table may implement `TableWithStructure`
specifying the structure, as an example:

```rust
impl TableWithStructure for ContractsRawCode {
    type Structure = Plain<Raw, Raw>;

    fn column() -> Column {
        Column::ContractsRawCode
    }
}
```

It is a definition of the structure for the `ContractsRawCode` table. It
has a plain structure meaning it simply encodes/decodes bytes and
stores/loads them into/from the storage. As a key codec and value codec,
it uses a `Raw` encoding/decoding that simplifies writing bytes and
loads them back into the memory without applying any serialization or
deserialization algorithm.

If the table implements `TableWithStructure` and the selected codec
satisfies all structure requirements, the corresponding storage traits
for that table are implemented on the `StructuredStorage` type.

### Codecs

Each structure allows customizing the key and value codecs. It allows
the use of different codecs for different tables, taking into account
the complexity and weight of the data and providing a way of more
optimal implementation.

That property may be very useful to perform migration in a more easier
way. Plus, it also can be a `no_std` migration potentially allowing its
fraud proving.

An example of migration:

```rust
/// Define the table for V1 value encoding/decoding.
impl TableWithStructure for ContractsRawCodeV1 {
    type Structure = Plain<Raw, Raw>;

    fn column() -> Column {
        Column::ContractsRawCode
    }
}

/// Define the table for V2 value encoding/decoding.
/// It uses `Postcard` codec for the value instead of `Raw` codec.
///
/// # Dev-note: The columns is the same.
impl TableWithStructure for ContractsRawCodeV2 {
    type Structure = Plain<Raw, Postcard>;

    fn column() -> Column {
        Column::ContractsRawCode
    }
}

fn migration(storage: &mut Database) {
    let mut iter = storage.iter_all::<ContractsRawCodeV1>(None);
    while let Ok((key, value)) = iter.next() {
        // Insert into the same table but with another codec.
        storage.storage::<ContractsRawCodeV2>().insert(key, value);
    }
}
```

### Structures

The structure of the table defines its behavior. As an example, a
`Plain` structure simply encodes/decodes bytes and stores/loads them
into/from the storage. The `SMT` structure builds a sparse merkle tree
on top of the key-value pairs.

Implementing a structure one time, we can apply it to any table
satisfying the requirements of this structure. It increases the re-usage
of the code and minimizes duplication.

It can be useful if we decide to create global roots for all required
tables that are used in fraud proving.

```rust
impl TableWithStructure for SpentMessages {
    type Structure = Plain<Raw, Postcard>;

    fn column() -> Column {
        Column::SpentMessages
    }
}
                 |
                 |
                \|/

impl TableWithStructure for SpentMessages {
    type Structure =
        Sparse<Raw, Postcard, SpentMessagesMerkleMetadata, SpentMessagesMerkleNodes>;

    fn column() -> Column {
        Column::SpentMessages
    }
}
```

### Side changes

#### `iter_all`
The `iter_all` functionality now accepts the table instead of `K` and
`V` generics. It is done to use the correct codec during
deserialization. Also, the table definition provides the column.

<img width="1234" alt="image"
src="https://github.com/FuelLabs/fuel-core/assets/18346821/74595ee2-bbd2-48a1-b0da-edf47abd7a4f">

#### Duplicated unit tests

The `fuel-core-storage` crate provides macros that generate unit tests.
Almost all tables had the same test like `get`, `insert`, `remove`,
`exist`. All duplicated tests were moved to macros. The unique one still
stays at the same place where it was before.

<img width="679" alt="image"
src="https://github.com/FuelLabs/fuel-core/assets/18346821/a4eb1fd9-c008-4ab0-902a-ab1fdbc855a8">

#### `StorageBatchMutate`

Added a new `StorageBatchMutate` trait that we can move to
`fuel-storage` crate later. It allows batch operations on the storage.
It may be more performant in some cases.

```rust
/// The traits allow work with the storage in batches.
/// Some implementations can perform batch operations faster than one by one.
pub trait StorageBatchMutate<Type: Mappable>: StorageMutate<Type> {
    /// Initialize the storage with batch insertion. This method is more performant than
    /// [`Self::insert_batch`] in some case.
    ///
    /// # Errors
    ///
    /// Returns an error if the storage is already initialized.
    fn init_storage(
        &mut self,
        set: &mut dyn Iterator<Item = (&Type::Key, &Type::Value)>,
    ) -> Result<()>;

    /// Inserts the key-value pair into the storage in batch.
    fn insert_batch(
        &mut self,
        set: &mut dyn Iterator<Item = (&Type::Key, &Type::Value)>,
    ) -> Result<()>;

    /// Removes the key-value pairs from the storage in batch.
    fn remove_batch(&mut self, set: &mut dyn Iterator<Item = &Type::Key>) -> Result<()>;
}
```

### Follow-up

It is one of the changes in the direction of the forkless upgrades for
state transition functions and fraud proofs. The idea behind this is
that the `fuel_core_executor::Executor` will work directly with the
`StructuredStorage` instead of the `Database`. It will perform only
state transition-related modifications to the storage, while all outside
modifications like updating of receipts, transition status, block
insertions, messages removing, and transaction storing will be a part of
another service/process.
  • Loading branch information
xgreenx authored Jan 19, 2024
1 parent 622e38b commit 3b487b1
Show file tree
Hide file tree
Showing 58 changed files with 3,467 additions and 2,146 deletions.
107 changes: 106 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

Description of the upcoming release here.


### Changed

- [#1591](https://github.com/FuelLabs/fuel-core/pull/1591): Simplify libp2p dependencies and not depend on all sub modules directly.
Expand All @@ -17,9 +16,115 @@ Description of the upcoming release here.
- [#1601](https://github.com/FuelLabs/fuel-core/pull/1601): Fix formatting in docs and check that `cargo doc` passes in the CI.

#### Breaking

- [#1593](https://github.com/FuelLabs/fuel-core/pull/1593) Make `Block` type a version-able enum
- [#1576](https://github.com/FuelLabs/fuel-core/pull/1576): The change moves the implementation of the storage traits for required tables from `fuel-core` to `fuel-core-storage` crate. The change also adds a more flexible configuration of the encoding/decoding per the table and allows the implementation of specific behaviors for the table in a much easier way. It unifies the encoding between database, SMTs, and iteration, preventing mismatching bytes representation on the Rust type system level. Plus, it increases the re-usage of the code by applying the same blueprint to other tables.

It is a breaking PR because it changes database encoding/decoding for some tables.

### StructuredStorage

The change adds a new type `StructuredStorage`. It is a wrapper around the key-value storage that implements the storage traits(`StorageInspect`, `StorageMutate`, `StorageRead`, etc) for the tables with blueprint. This blueprint works in tandem with the `TableWithBlueprint` trait. The table may implement `TableWithBlueprint` specifying the blueprint, as an example:

```rust
impl TableWithBlueprint for ContractsRawCode {
type Blueprint = Plain<Raw, Raw>;

fn column() -> Column {
Column::ContractsRawCode
}
}
```

It is a definition of the blueprint for the `ContractsRawCode` table. It has a plain blueprint meaning it simply encodes/decodes bytes and stores/loads them into/from the storage. As a key codec and value codec, it uses a `Raw` encoding/decoding that simplifies writing bytes and loads them back into the memory without applying any serialization or deserialization algorithm.

If the table implements `TableWithBlueprint` and the selected codec satisfies all blueprint requirements, the corresponding storage traits for that table are implemented on the `StructuredStorage` type.

### Codecs

Each blueprint allows customizing the key and value codecs. It allows the use of different codecs for different tables, taking into account the complexity and weight of the data and providing a way of more optimal implementation.

That property may be very useful to perform migration in a more easier way. Plus, it also can be a `no_std` migration potentially allowing its fraud proving.

An example of migration:

```rust
/// Define the table for V1 value encoding/decoding.
impl TableWithBlueprint for ContractsRawCodeV1 {
type Blueprint = Plain<Raw, Raw>;

fn column() -> Column {
Column::ContractsRawCode
}
}

/// Define the table for V2 value encoding/decoding.
/// It uses `Postcard` codec for the value instead of `Raw` codec.
///
/// # Dev-note: The columns is the same.
impl TableWithBlueprint for ContractsRawCodeV2 {
type Blueprint = Plain<Raw, Postcard>;

fn column() -> Column {
Column::ContractsRawCode
}
}

fn migration(storage: &mut Database) {
let mut iter = storage.iter_all::<ContractsRawCodeV1>(None);
while let Ok((key, value)) = iter.next() {
// Insert into the same table but with another codec.
storage.storage::<ContractsRawCodeV2>().insert(key, value);
}
}
```

### Structures

The blueprint of the table defines its behavior. As an example, a `Plain` blueprint simply encodes/decodes bytes and stores/loads them into/from the storage. The `SMT` blueprint builds a sparse merkle tree on top of the key-value pairs.

Implementing a blueprint one time, we can apply it to any table satisfying the requirements of this blueprint. It increases the re-usage of the code and minimizes duplication.

It can be useful if we decide to create global roots for all required tables that are used in fraud proving.

```rust
impl TableWithBlueprint for SpentMessages {
type Blueprint = Plain<Raw, Postcard>;

fn column() -> Column {
Column::SpentMessages
}
}
|
|
\|/

impl TableWithBlueprint for SpentMessages {
type Blueprint =
Sparse<Raw, Postcard, SpentMessagesMerkleMetadata, SpentMessagesMerkleNodes>;

fn column() -> Column {
Column::SpentMessages
}
}
```

### Side changes

#### `iter_all`
The `iter_all` functionality now accepts the table instead of `K` and `V` generics. It is done to use the correct codec during deserialization. Also, the table definition provides the column.

#### Duplicated unit tests

The `fuel-core-storage` crate provides macros that generate unit tests. Almost all tables had the same test like `get`, `insert`, `remove`, `exist`. All duplicated tests were moved to macros. The unique one still stays at the same place where it was before.

#### `StorageBatchMutate`

Added a new `StorageBatchMutate` trait that we can move to `fuel-storage` crate later. It allows batch operations on the storage. It may be more performant in some cases.

- [#1573](https://github.com/FuelLabs/fuel-core/pull/1573): Remove nested p2p request/response encoding. Only breaks p2p networking compatibility with older fuel-core versions, but is otherwise fully internal.


## [Version 0.22.0]

### Added
Expand Down
43 changes: 39 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ async-trait = "0.1"
cynic = { version = "2.2.1", features = ["http-reqwest"] }
clap = "4.1"
derive_more = { version = "0.99" }
enum-iterator = "1.2"
hyper = { version = "0.14.26" }
primitive-types = { version = "0.12", default-features = false }
rand = "0.8"
Expand All @@ -100,6 +101,8 @@ tracing-attributes = "0.1"
tracing-subscriber = "0.3"
serde = "1.0"
serde_json = "1.0"
strum = "0.25"
strum_macros = "0.25"
# enable cookie store to support L7 sticky sessions
reqwest = { version = "0.11.16", default-features = false, features = ["rustls-tls", "cookies"] }
mockall = "0.11"
Expand Down
1 change: 1 addition & 0 deletions ci_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ cargo +nightly fmt --all -- --check &&
cargo sort -w --check &&
source .github/workflows/scripts/verify_openssl.sh &&
cargo clippy --all-targets --all-features &&
cargo doc --all-features --workspace &&
cargo make check --locked &&
cargo make check --all-features --locked &&
cargo check -p fuel-core-types --target wasm32-unknown-unknown --no-default-features &&
Expand Down
2 changes: 1 addition & 1 deletion crates/chain-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fuel-core-storage = { workspace = true }
fuel-core-types = { workspace = true, default-features = false, features = ["serde"] }
hex = { version = "0.4", features = ["serde"] }
itertools = { workspace = true }
postcard = { version = "1.0", features = ["alloc"] }
postcard = { workspace = true, features = ["alloc"] }
rand = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
Expand Down
8 changes: 3 additions & 5 deletions crates/fuel-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async-trait = { workspace = true }
axum = { workspace = true }
clap = { workspace = true, features = ["derive"] }
derive_more = { version = "0.99" }
enum-iterator = "1.2"
enum-iterator = { workspace = true }
fuel-core-chain-config = { workspace = true }
fuel-core-consensus-module = { workspace = true }
fuel-core-database = { workspace = true }
Expand All @@ -41,16 +41,14 @@ futures = { workspace = true }
hex = { version = "0.4", features = ["serde"] }
hyper = { workspace = true }
itertools = { workspace = true }
postcard = { workspace = true, features = ["use-std"] }
rand = { workspace = true }
rocksdb = { version = "0.21", default-features = false, features = [
"lz4",
"multi-threaded-cf",
], optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
strum = "0.24"
strum_macros = "0.24"
strum = { workspace = true }
strum_macros = { workspace = true }
tempfile = { workspace = true, optional = true }
thiserror = "1.0"
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
Expand Down
Loading

0 comments on commit 3b487b1

Please sign in to comment.