| Crates.io | calimero-store |
| lib.rs | calimero-store |
| version | 0.8.0-rc.10 |
| created_at | 2025-08-31 11:52:45.015361+00 |
| updated_at | 2025-09-17 13:50:18.026257+00 |
| description | Core Calimero infrastructure and tools |
| homepage | |
| repository | https://github.com/calimero-network/core |
| max_upload_size | |
| id | 1818453 |
| size | 155,346 |
The store library is a versatile and efficient key-value storage system designed for managing application state and transactions. It provides a flexible and extensible architecture for storing, retrieving, and manipulating data across different storage backends.
Key features of the store library include:
The Database trait defines the core interface for interacting with the
underlying storage system. It provides methods for basic CRUD operations,
iteration, and transaction application.
Key operations:
open(): Initialise the database with a given configurationhas(), get(), put(), delete(): Basic key-value operationsiter(): Create an iterator for range queriesapply(): Apply a transaction to the databaseThe RocksDB struct implements this trait, providing a concrete implementation
using the RocksDB storage engine.
The library uses a flexible key structure defined by the Key type and
associated traits:
KeyComponent: Defines a part of a key with a specific lengthKeyComponents: Combines multiple KeyComponents into a complete keyAsKeyParts: Converts a key into its column and componentsFromKeyParts: Constructs a key from its componentsThis structure allows for efficient encoding and decoding of complex keys, supporting various data models.
The DataType trait defines how values are serialised and deserialised. The
Entry trait combines a key with its associated data type, providing a complete
representation of a stored item.
Predefined entry types include:
ContextMeta: Metadata for a contextContextState: State data for a contextContextIdentity: Identity information for a contextContextTransaction: Transaction data for a contextGeneric: A generic key-value pairThe library implements a layered architecture for managing data access and modifications:
Layer: Base trait for all layersReadLayer: Provides read-only access to the underlying dataWriteLayer: Extends ReadLayer with write operationsThe Transaction struct represents a set of operations to be applied
atomically. It supports putting and deleting entries, as well as merging with
other transactions.
The library provides a flexible iteration system:
Iter: Base iterator typeDBIter: Trait for database-specific iteratorsStoreHandle to interact with the storeTemporal layergraph TD
A[Application] --> B[StoreHandle]
B --> C[Store]
C --> D[Database Backend]
D --> E[RocksDB and others]
B --> F
F --> C
B --> G[Read-Only Layer]
G --> C
subgraph F[Temporal Layer]
H[Transaction]
end
Columns: The library uses a column-based structure to organise different
types of data (Identity, State, Transaction, Generic).
Context: Many operations are context-aware, allowing for isolation between different application contexts.
Serialisation: The library supports custom serialisation through the
DataType trait.
Error handling: The library uses the eyre crate for error handling,
providing detailed error context.
Generic programming: Extensive use of generic programming allows for flexibility and type safety.
Temporal layer: Allows for accumulating changes in memory before committing to the underlying storage, useful for managing complex transactions or temporary state changes as an atomic operation.
Read-only layer: Provides a way to ensure that certain operations are read-only, enhancing safety and preventing accidental modifications.
Custom key types: The key system allows for creating complex, typed keys that can encode multiple pieces of information efficiently.
Flexible iteration: The iterator system supports both unstructured and structured data, allowing for type-safe iteration over complex data structures.
Transaction support: Built-in support for creating and applying transactions, ensuring atomic updates across multiple operations.
The database interface is a fundamental component of the store library, serving
as the foundation for all data storage and retrieval operations. Defined by the
Database trait, this interface provides a uniform way to interact with the
underlying storage system, abstracting away the specifics of any particular
database implementation.
At its core, the database interface is designed to support a key-value storage
model, where each piece of data is associated with a unique key. This model is
further enhanced by the concept of columns, which allow for logical separation
of different types of data within the same database. The interface defines four
primary columns: Identity, State, Transaction, and Generic, each serving
a specific purpose in the overall data architecture.
The Database trait specifies a set of primary operations that any implementing
storage system must provide. These operations include checking for the existence
of a key (has()), retrieving data associated with a key (get()), storing new
data or updating existing data (put()), and removing data (delete()). These
operations therefore align to the well-known, fundamental "CRUD" actions. Each
of these operations is column-aware, allowing for efficient organisation and
retrieval of data based on its type or purpose.
In addition to these basic operations, the interface also defines methods for
more complex interactions. The iter() method allows for iteration over a range
of keys within a specific column, enabling efficient scanning and querying of
data. This is particularly useful for operations that need to process multiple
related entries, such as retrieving all state data for a particular context.
One of the most important features of the database interface is its support for
transactions through the apply() method. This method takes a Transaction
object, which represents a set of changes to be applied atomically to the
database. By supporting transactions, the interface ensures that complex
operations involving multiple keys can be performed consistently, maintaining
data integrity even in the face of concurrent access or system failures.
The interface is designed with flexibility and extensibility in mind. It uses
Rust's powerful type system to ensure type safety and to allow for custom key
and value types. The use of the eyre crate for error handling provides rich
context for any errors that might occur during database operations, aiding in
debugging and error recovery.
Importantly, the database interface is designed to be asynchronous and
thread-safe. This is enforced by applying Send + Sync bounds to the Database
trait, ensuring that implementations can be safely shared and used across
multiple threads. This design choice allows for high-performance, concurrent
access to the database, which is crucial for applications dealing with large
amounts of data or high request volumes.
While the interface defines the contract that any database implementation must fulfill, it doesn't prescribe how these operations should be implemented. This abstraction allows the library to support different storage backends without changing the core logic of the store system. Currently, the library provides an implementation for RocksDB, a high-performance key-value store, but the design allows for easy addition of other backends in the future.
The key structure in the store library is a sophisticated and flexible system designed to efficiently encode and manage complex keys for data storage and retrieval. This structure is fundamental to the library's ability to organise and access data in a type-safe and performant manner.
At the heart of the key structure are several interrelated traits and types:
KeyComponent: This trait defines a single part of a key. It associates a
type with a specific length, allowing for fixed-size key components. This is
crucial for efficient storage and retrieval in many database systems.
KeyComponents: Building on KeyComponent, this trait allows for the
composition of multiple components into a single key. It uses Rust's type
system to calculate the total length of the combined components at
compile-time, ensuring type safety and optimal performance.
Key<T>: This is a newtype wrapper around a GenericArray of bytes. The
type parameter T must implement KeyComponents, which determines the size
and structure of the key.
AsKeyParts: This trait defines how a key type can be decomposed into its
column and components. It's essential for the library's ability to work with
keys in a generic manner while still preserving type information.
FromKeyParts: The counterpart to AsKeyParts, this trait allows for the
construction of a key from its components. It includes an associated Error
type, allowing for type-specific error handling during key construction.
The key structure's design prioritises both flexibility and type safety. By using Rust's generic programming features, the library allows for the creation of complex, multi-component keys while still maintaining strong type checking at compile-time. This prevents many potential runtime errors related to key formatting or interpretation.
For example, a key might be composed of a context ID and a state key:
pub struct ContextState(Key<(ContextId, StateKey)>);
Here, ContextId and StateKey are separate KeyComponent types, combined
into a single key structure. The library can then provide methods to safely
access these components:
use calimero_primitives::context::ContextId;
impl ContextState {
pub fn context_id(&self) -> ContextId { ... }
pub fn state_key(&self) -> [u8; 32] { ... }
}
The key structure is designed with performance in mind. By using fixed-size components and compile-time length calculation, the library can avoid runtime allocations and provide predictable performance characteristics. This is particularly important for database operations where key comparisons and sorting are frequent operations.
The key structure integrates seamlessly with the database interface. The
AsKeyParts trait allows the database to efficiently determine the column for a
given key, which is crucial for operations like put(), get(), and
delete(). Similarly, the FromKeyParts trait enables the creation of
structured keys from raw byte data, which is essential for operations like
iteration and range queries.
The library provides several predefined key types that leverage this structure:
ContextMetaContextIdentityContextStateContextTransactionGenericThese predefined types demonstrate how the key structure can be used to create semantically meaningful keys that encode multiple pieces of information.
The store library implements a sophisticated system for handling diverse data
types and entries, providing a flexible and type-safe approach to storing and
retrieving various kinds of information. This system is built around two key
traits: DataType and Entry, which work together to define how data is
serialised, deserialised, and associated with keys in the store.
DataType traitThe DataType trait is at the core of the library's value handling system. It
defines how different types of data are converted to and from raw bytes for
storage. This trait has two primary methods:
from_slice(): This method converts a slice of bytes into the specific
data type.
as_slice(): This method converts the data type back into a slice of
bytes.
The DataType trait is generic over a lifetime 'a, allowing it to handle both
owned and borrowed data efficiently. It also includes an associated Error
type, enabling implementors to define type-specific error handling for
serialisation and deserialisation operations.
This design allows for flexibility in how data is stored and retrieved. Implementors can choose their own serialisation formats, optimise for specific use cases, or integrate with existing data structures seamlessly.
The design allows for easy extensibility, as new data types can be created by
implementing the DataType trait. This extensibility is crucial for adapting
the library to various use cases and data models without modifying the core
library code.
Entry traitThe Entry trait builds upon DataType to create a complete representation of
a stored item. It associates a key type with a data type, effectively defining
the structure of entries in the store. The Entry trait has two associated
types:
Key: The type used as the key for this entry, which must implement
AsKeyParts.
DataType: The type of data stored for this entry, which must implement
the DataType trait.
The Entry trait also includes a key() method, which returns a reference to
the key for the entry. This design allows for efficient key access without
unnecessary copying or cloning.
The library provides several predefined entry types that demonstrate the power and flexibility of this system:
ContextMeta: Stores metadata about a context, including an application
ID and the hash of the last transaction.
ContextState: Represents the state data for a context, storing it as a
raw slice of bytes.
ContextIdentity: Holds identity information for a context, including an
optional private key.
ContextTransaction: Stores information about a transaction, including
the method name, payload, and the hash of the prior transaction.
Generic: A flexible entry type for storing arbitrary data associated
with a scope and fragment.
Each of these predefined types implements both the DataType and
PredefinedEntry traits, with the latter being a convenience trait that
implements Entry for all defined keys, with the entry key as the key it's
defined for.
The combination of the DataType and Entry traits with Rust's type system
provides a high degree of flexibility while maintaining strong type safety. This
design allows developers to define custom data types and entries that fit their
specific needs, while the compiler ensures that keys and values are used
correctly throughout the application.
For instance, the ContextState entry type demonstrates how the system can
handle raw byte data efficiently:
pub struct ContextState<'a> {
pub value: Slice<'a>,
}
impl<'a> DataType<'a> for ContextState<'a> {
type Error = io::Error;
fn from_slice(slice: Slice<'a>) -> Result<Self, Self::Error> {
Ok(Self { value: slice })
}
fn as_slice(&'a self) -> Result<Slice<'a>, Self::Error> {
Ok(self.value.as_ref().into())
}
}
This implementation allows for efficient storage and retrieval of raw state data without unnecessary copying or processing.
The data types and entries system integrates seamlessly with other components of the library, such as the key structure and the database interface. This integration allows for type-safe operations across the entire storage system, from key creation to value serialisation and database interaction.
The store library implements a sophisticated layered architecture that provides flexible and powerful data access and manipulation capabilities. This layered approach allows for separation of concerns, enhanced functionality, and the ability to compose different behaviours in a modular fashion.
The layer system is built around several key traits:
Layer: This is the base trait for all layers. It defines an associated
type Base which represents the underlying layer that this layer is built
upon.
ReadLayer: This trait extends Layer and provides read-only access to
the data store. It defines methods for checking if a key exists (has()),
retrieving data (get()), and creating iterators (iter()).
WriteLayer: This trait further extends ReadLayer to provide write
capabilities. It includes methods for putting data (put()), deleting data
(delete()), applying transactions (apply()), and committing changes
(commit()).
These traits form a hierarchy that allows layers to build upon each other, adding or modifying functionality at each level.
The Store struct serves as the foundational layer in this architecture. It
implements both ReadLayer and WriteLayer, providing direct access to the
underlying database. The Store is typically backed by a concrete database
implementation, such as RocksDB.
The library includes several specialised layers that provide additional functionality:
Temporal layer
The Temporal layer is a key component of the library's transactional
capabilities. It acts as a buffer between the application and the underlying
store, accumulating changes without immediately applying them to the base
layer.
This allows for complex operations to be built up over time and then committed atomically.
Key features of the Temporal layer include:
commit() method is called, all accumulated
changes are applied to the base layer in a single operation.The Temporal layer is particularly useful for scenarios where you need to
make multiple related changes and ensure they are all applied together, or
not at all.
Read-only layer
The ReadOnly layer provides a way to restrict access to read-only
operations. This is useful for ensuring that certain parts of your code
cannot accidentally modify the store, enhancing safety and preventing
unintended side effects.
The ReadOnly layer implements ReadLayer but not WriteLayer, effectively
preventing any write operations from being performed through it.
One of the most powerful aspects of the layer system is the ability to compose layers. Layers can be stacked on top of each other, each adding or modifying functionality.
This composability is achieved through the use of the Base associated type in
the Layer trait. Each layer specifies what type of layer it's built on top of,
allowing for type-safe composition of layers.
The layer system integrates seamlessly with other components of the library:
It works with the key and entry system, allowing for type-safe operations on structured data.
It supports the iteration system, allowing for efficient scanning and querying of data.
It interacts with the transaction system, particularly in the case of the
Temporal layer.
The layer system is designed to be extensible. New layers can be easily created by implementing the appropriate traits. This allows for the creation of specialised layers for specific use cases, such as logging layers, caching layers, or layers that implement specific business logic.
Another interesting one is Tee (named after the Linux command), which wraps
two Layers, reading from one and falling back on to the other when it doesn't
exist, or writing to both.
Notably, this is part of the design considerations that influenced the decision
of why WriteLayer::apply() takes a &Transaction.
The layered architecture does introduce some overhead, particularly in terms of method call indirection. However, this is generally outweighed by the benefits in terms of flexibility and functionality. Moreover, the use of Rust's zero-cost abstractions means that much of this overhead can be optimised away at compile time.
The store library implements a robust transaction system that allows for atomic, consistent updates to the data store. This system is crucial for maintaining data integrity, especially in scenarios involving complex operations or concurrent access.
At the heart of the transaction system is the Transaction struct. This
structure represents a set of operations to be applied atomically to the store.
The key features of a transaction include:
Atomic operations: All operations in a transaction are applied together, or not at all. This ensures consistency in the face of failures or interruptions.
Isolation: Changes made within a transaction are not visible to other parts of the system until the transaction is committed.
Durability: Once a transaction is committed, its changes are permanently
stored and will survive system failures (depending, of course, upon the
Database implementation in use).
The Transaction struct is designed to be both efficient and flexible:
It uses a BTreeMap to store operations, allowing for efficient lookups and
range queries.
Each entry in the map consists of an Entry (representing a key) and an
Operation (representing the action to be performed).
The Operation enum can represent either a Put operation (to insert or
update data) or a Delete operation.
The Transaction struct provides several key methods for building and
manipulating transactions:
put(): Adds a put operation to the transaction, associating a key with
a value.
delete(): Adds a delete operation to the transaction for a specific
key.
get(): Retrieves the operation associated with a specific key in the
transaction.
merge(): Combines the operations from another transaction into this
one.
iter(): Creates an iterator over all operations in the transaction.
iter_range(): Creates an iterator over a range of operations in the
transaction.
These methods allow for flexible construction of complex transactions involving multiple operations across different keys.
Transactions are deeply integrated with the layer system in the library:
Write layer: The WriteLayer trait includes an apply() method that
takes a Transaction as an argument. This method is responsible for applying
the transaction's operations to the underlying storage.
Temporal layer: The Temporal layer uses transactions internally to
accumulate changes before committing them to the underlying store. This
allows for efficient batching of operations and provides a natural way to
implement transactional semantics.
The execution of a transaction typically follows these steps:
A new Transaction is created.
Operations (put() and delete()) are added to the transaction.
The transaction is passed to a layer's apply() method.
The layer (often a Temporal layer) processes the transaction's operations.
If using a Temporal layer, the changes are accumulated until commit() is
called.
Upon commit, all changes are applied atomically to the underlying store.
The transaction system helps ensure consistency and isolation:
Consistency: By grouping related operations together, transactions help maintain invariants and consistency rules in the data.
Isolation: Changes made within a transaction (especially when using the
Temporal layer) are not visible to other parts of the system until the
transaction is committed.
The transaction system is designed with performance in mind:
The use of a BTreeMap for storing operations allows for efficient lookups
and range queries.
The ability to batch multiple operations into a single transaction can significantly reduce the number of interactions with the underlying storage system.
The Temporal layer's ability to accumulate changes before committing allows
for efficient batching of writes.
The store library implements a sophisticated iterator system that provides efficient and flexible means of traversing and processing stored data. This system is crucial for operations that need to work with ranges of data, such as scanning, filtering, or aggregating over large datasets.
The iterator system is built around several key types and traits:
Iter: This is the primary iterator type in the library. It's generic
over two type parameters, K and V, which represent the key and value
types respectively.
DBIter: This trait defines the core functionality that any
database-specific iterator must implement. It includes methods for moving to
the next item (next()) and reading the current value (read()). In this
way it is similar to Rust's Iterator trait (however, see the
limitations section below).
Structured and Unstructured: These types are used to differentiate
between iterators that work with typed, structured data and those that work
with raw byte slices.
The Iter struct provides several important methods:
keys(): Returns an iterator over just the keys.
entries(): Returns an iterator over key-value pairs.
structured_key(): Converts an unstructured key iterator into a
structured one.
structured_value(): Converts an unstructured value iterator into a
structured one.
These methods allow for flexible processing of data, enabling operations on keys alone, on full entries, or on structured representations of the data.
One of the most powerful features of the iterator system is its use of Rust's
type system to provide type-safe iteration. The Structured type parameter
allows the iterator to work with strongly-typed keys and values, while still
allowing for efficient, low-level storage.
For example, an iterator might be defined as:
Iter<Structured<ContextState>, Structured<StateData>>
This type ensures that the iterator will yield keys of type ContextState and
values of type StateData, providing compile-time guarantees about the types of
data being processed.
The iterator system is deeply integrated with other components of the library:
Layer integration: The ReadLayer trait includes an iter method that
returns an Iter. This allows each layer to provide its own implementation
of iteration, potentially adding additional functionality or optimisations.
Transaction integration: The Transaction struct provides methods for
creating iterators over its operations. This allows for efficient processing
of pending changes in a transaction.
The iterator system supports efficient range queries through the use of a
start key in the iter() method of ReadLayer. This allows for iteration to
begin at a specific point in the key space, enabling efficient partial scans of
the data.
The iterator system is designed with performance in mind:
Lazy evaluation: Iterators are lazy, meaning they only do work when asked for the next item. This allows for efficient processing of large datasets without unnecessary memory usage, and allows for early stopping when the desired item is found.
Zero-cost abstractions: The use of Rust's trait system and generic programming allows many of the abstractions to be resolved at compile-time, resulting in efficient machine code.
Custom implementations: The DBIter trait allows for database-specific
optimisations in the underlying iteration logic.
The iterator system is designed to be extensible:
Custom iterators: New iterator types can be easily created by
implementing the DBIter trait.
Composition: The IterPair struct allows for the composition of multiple
iterators, enabling complex iteration patterns.
The iterator system supports a wide range of use cases:
Scanning: Efficiently scan through large portions of the data store.
Filtering: Iterate over data and filter based on key or value properties.
Aggregation: Perform aggregations or reductions over ranges of data.
Prefix queries: Efficiently query all keys with a certain prefix.
While powerful, users of the iterator system should be aware of certain considerations:
Consistency: In a multi-threaded environment, iterators provide a point-in-time view and may not reflect concurrent modifications. If an iterator is to be shared across threads, care should be taken to ensure consistency.
Resource usage: Long-running iterations may hold resources for extended periods. Care should be taken in concurrent scenarios.