# donadb-x
[](https://crates.io/crates/donadb-x)
[](https://docs.rs/donadb-x)
[](#license)
[](https://github.com/truth-linked/truthlinked/actions)
A lock-free, memory-mapped storage engine built for blockchain validator state. DonaDbX combines a commutative append log, atomic double-buffer swapping, parallel BLAKE3 folding, and crash-safe segment management into a single coherent API.
---
## Why DonaDbX
Most general-purpose key-value stores are optimised for balanced read/write workloads with durable transactions. Blockchain validator state has different requirements:
- **Writes are append-only and commutative.** Within a block, transaction order does not affect the final state root.
- **Reads always target the previous committed block.** No thread ever reads uncommitted state from the current active buffer.
- **State roots must be fast.** The Merkle root must be computable without a full tree traversal after every block.
- **Crash recovery must be deterministic.** A node that restarts mid-block must replay to exactly the same state as peers.
DonaDbX is designed around these constraints. The result is a storage layer that outperforms RocksDB, LMDB, and redb on every workload that matters for validator operation.
---
## Benchmark Results
All numbers measured on Linux x86\_64, 8 logical CPUs, AVX2, in-memory mmap, no fsync per write.
### donadb-X standalone
| Pure write (1M ops, fetch\_add + memcpy only) | **2.36M ops/s** |
| Random read (200K over 100K committed keys) | **1.30M ops/s** |
| Mixed 80% write / 20% read (200K ops) | **2.06M ops/s** |
| State root latency under concurrent writes | **avg 0 µs, max 3 µs** |
| MVCC chain walk (depth 10 000) | **1 000 ns/walk** |
| Crash recovery replay (500K records) | **536 ms** |
| Concurrent write, 8 threads (4M ops) | **up to 10.23M ops/s** |
### donadb-X vs field (bench\_vs\_all)
| Sequential write 500K | **3 067 K** | 216 K | 202 K | 37 K | 20 K |
| Random read 200K | **1 136 K** | 200 K | 619 K | 417 K | 201 K |
| Concurrent write 8T | **804 K** | 35 K | 369 K¹ | 20 K¹ | 17 K |
| Mixed 80/20 200K | **905 K** | 374 K | 236 K | — | 33 K |
¹ LMDB and redb are single-writer by design; shown at equivalent total write volume.
> Zero stubs. Zero mocking. Real wall-clock. Real disk. Source in [`src/bin/bench_vs_all.rs`](src/bin/bench_vs_all.rs).
---
## Architecture
DonaDbX is built from five cooperating subsystems:
```
┌──────────────────────────────────────────────────────────────┐
│ DonaDbX API │
└───────────┬──────────────────────────────────┬───────────────┘
│ │
┌─────────▼──────────┐ ┌──────────▼──────────┐
│ 1. ExecutionFrame │ │ 3. DualBufferEngine │
│ Isolated write │ │ N-shard ArcSwap; │
│ arena; rollback │ │ atomic buffer swap │
└─────────┬──────────┘ └──────┬───────────────┘
│ │
│ ┌──────────┼──────────────┐
│ │ │ │
┌─────────▼──────────┐ ┌────▼───────┐ ┌▼────────────┐
│ 2. CommutativeLog │ │ 4. Rayon │ │ 5. Segment │
│ Lock-free mmap │ │ fold │ │ Manager │
│ fetch_add + │ │ Parallel │ │ Rotation, │
│ memcpy append │ │ BLAKE3 + │ │ manifest, │
│ posix_fallocate │ │ XOR root │ │ recovery │
└────────────────────┘ └────────────┘ └─────────────┘
```
| 1 | `ExecutionFrame` | Isolated write arena with zero-copy rollback. Writes are visible via `get()` immediately (active-log scan); visible via the index after `commit()`. |
| 2 | `CommutativeLog` | Memory-mapped append log. One `fetch_add` reserves space; one `memcpy` fills it. `posix_fallocate` pre-allocates physical blocks so disk-full surfaces at segment creation, not mid-write. |
| 3 | `DualBufferEngine` | N shards (one per logical CPU), each with an `ArcSwap<CommutativeLog>`. `commit()` atomically swaps all shards; a dedicated fold thread processes the sealed logs without blocking writers. |
| 4 | Rayon fold pool | After each swap, runs parallel `BLAKE3(value)` hashing across all records. Maintains a commutative XOR accumulator `key XOR BLAKE3(value)` for the state root. |
| 5 | Segment manager | Rotates the active log to a sealed segment when the fill threshold is crossed. Writes a crash-safe JSON manifest via atomic `write-tmp → rename`. Replays the active log on reopen. |
### Record layout
Every record written to the mmap has this 24-byte header followed by the key and value:
```
Offset Size Field
──────────────────────────────────────
0 4 magic 0xC047_C047
4 4 vlen value length in bytes
8 8 prev_offset MVCC chain pointer (0 = first write)
16 8 height block height at write time
────────────────────────────────────── (24 bytes)
24 32 key
56 N value
```
The first 8 bytes of every segment file hold the `committed_offset` as a little-endian `u64`, used by crash recovery.
---
## Quick Start
Add to `Cargo.toml`:
```toml
[dependencies]
donadb-x = "0.1"
```
### Basic usage
```rust,no_run
use donadb_x::{DonaDbX, Config};
let db = DonaDbX::open("./state", Config::default()).unwrap();
let key = [1u8; 32];
db.put(key, b"hello").unwrap();
// commit() swaps the active buffer and launches an async fold.
let ack = db.commit(1).unwrap();
// wait() blocks until the fold completes and returns the 32-byte state root.
let root = ack.wait();
assert_eq!(db.get(&key).unwrap(), b"hello");
println!("block 1 state root: {}", hex::encode(root));
```
### Multi-threaded writes with `BlockWriter`
For high-throughput ingestion, acquire one `BlockWriter` per thread per block. Each writer holds a direct `Arc<CommutativeLog>` — no `ArcSwap` overhead per write, zero cross-shard contention.
```rust,no_run
use donadb_x::{DonaDbX, Config};
use std::sync::Arc;
let db = Arc::new(DonaDbX::open("./state", Config::default()).unwrap());
let root = db.commit(42).unwrap().wait();
println!("block 42 state root: {}", hex::encode(root));
```
### MVCC point-in-time reads
```rust,no_run
use donadb_x::{DonaDbX, Config};
let db = DonaDbX::open("./state", Config::default()).unwrap();
let key = [7u8; 32];
db.put(key, b"v1").unwrap();
db.commit(10).unwrap().wait();
db.put(key, b"v2").unwrap();
db.commit(20).unwrap().wait();
// Returns "v1" — the value current at the end of block 10.
assert_eq!(db.get_at(&key, 10).unwrap(), b"v1");
// Returns "v2" — the value current at the end of block 20.
assert_eq!(db.get_at(&key, 20).unwrap(), b"v2");
```
### Isolated execution frames with rollback
```rust,no_run
use donadb_x::{DonaDbX, Config};
let db = DonaDbX::open("./state", Config::default()).unwrap();
let key = [3u8; 32];
db.put(key, b"base").unwrap();
db.commit(1).unwrap().wait();
// Begin a speculative frame.
let frame = db.begin_frame();
frame.put(key, b"speculative").unwrap();
// The write is immediately visible via get() (active-log scan).
assert_eq!(db.get(&key).unwrap(), b"speculative");
// Abort rolls back the write; the previous committed value is restored.
frame.abort();
assert_eq!(db.get(&key).unwrap(), b"base");
```
---
## API Reference
### `DonaDbX::open(dir, cfg)`
Opens or creates a database at `dir`. On first call, creates the directory and initialises a fresh segment. On subsequent calls, reads the manifest, memory-maps all sealed segments, and replays the active log to restore the in-memory index and state root. Crash recovery is automatic — no separate repair step is needed.
### `put(key, value) → DbResult<u64>`
Lock-free append. Performs a single `fetch_add` to reserve space in the active log, then copies the key and value in. Returns the byte offset of the new record. Writes are immediately visible to `get()` via the active-log scan.
### `commit(height) → DbResult<CommitAck>`
Atomically swaps the active buffer across all shards and dispatches a fold to the background thread. Returns immediately; the fold runs concurrently with the next block's writes. Call `ack.wait()` before producing a block header to ensure the state root is final.
### `get(key) → DbResult<Vec<u8>>`
Three-step lookup:
1. **Index** (O(1)) — covers all keys folded in a prior `commit()`.
2. **Active-log scan** — covers writes in the current uncommitted batch.
3. **Sealed segments** — covers keys rotated out of the active log.
### `get_at(key, height) → DbResult<Vec<u8>>`
MVCC read. Walks the `prev_offset` chain to find the last write to `key` at or before `height`. O(v) where v is the number of versions of the key.
### `state_root() → [u8; 32]`
Returns `BLAKE3(XOR accumulator)` in O(1). The accumulator is maintained incrementally by the fold thread; this call reads it without any hashing.
### `begin_frame() → ExecutionFrame`
Opens a speculative write arena. Writes are appended to the active log immediately but index updates are deferred until `frame.commit()` + fold. `frame.abort()` zero-fills the reserved region and rewinds the write offset as if the writes never happened.
### `writer(shard_id) → BlockWriter`
Returns a write handle for `shard_id % num_shards`. Holds a direct `Arc<CommutativeLog>` — no shared state touched per write beyond the shard's own `write_offset` atomic. One `BlockWriter` per thread per block; do not share across threads.
---
## Configuration
```rust,no_run
use donadb_x::Config;
let cfg = Config {
// Maximum size of a single segment file in bytes.
// posix_fallocate pre-allocates this space on segment creation.
// Default: 2 GiB. Choose to fit one full block's worth of writes.
buffer_size: 512 << 20, // 512 MiB
// Fraction of buffer_size at which segment rotation is triggered.
// Checked after every commit(). Range: (0.0, 1.0].
// Default: 0.80 (rotate at 80 % full).
rotate_threshold: 0.75,
// Number of Rayon threads for the fold pool.
// None = auto (half of logical CPU count, minimum 2).
fold_threads: Some(4),
};
```
---
## Crash Safety
DonaDbX does not require an explicit shutdown sequence. The segment file is its own write-ahead log.
**Durable anchor:** The first 8 bytes of `seg_active.log` store the `committed_offset` as a little-endian `u64`. Only records below this offset are replayed on startup. Everything above it (partial writes from a crash) is silently discarded.
**When is data durable?** A write is durable when:
1. `commit()` has been called for the block containing that write, **and**
2. `ack.wait()` has returned (fold complete, `committed_offset` advanced), **and**
3. The OS has flushed the mmap page containing offset 0 to disk.
DonaDbX calls `mmap::flush_async()` after each fold. On a controlled shutdown the OS flushes remaining dirty pages. On a hard power loss, recovery replays from the last successfully flushed checkpoint — data is not lost, but up to one fold's worth of records may be re-replayed. Re-replay is idempotent: the rebuilt state root is identical.
**Disk-full protection:** `posix_fallocate` is called at segment creation time. If the filesystem cannot provide the required space, `DbError::Io(ENOSPC)` is returned from `open()` or `rotate()` — never as a `SIGBUS` mid-write.
**Manifest atomicity:** The segment manifest is written via `write-tmp → rename`, which is atomic on POSIX filesystems. A crash during a manifest write leaves either the old or new manifest intact — never a partial file.
---
## Feature Flags
| `rocks` | `bench_vs_rocks` binary (requires RocksDB system library) |
| `allbenches` | `bench_vs_all` binary (RocksDB + redb + sled + LMDB + PebbleDB) |
Default build has no optional dependencies.
---
## Running the Benchmarks
```bash
# donadb-X standalone
cargo run --release --bin bench
# donadb-X vs RocksDB only
cargo run --release --bin bench_vs_rocks --features rocks
# donadb-X vs all engines
cargo run --release --bin bench_vs_all --features allbenches
```
---
## Platform Notes
- **Linux** — primary target. `posix_fallocate` is used for physical block pre-allocation.
- **macOS** — builds and runs. `posix_fallocate` falls back to `fcntl(F_PREALLOCATE)` via libc; behaviour is equivalent.
- **Windows** — not currently supported (`posix_fallocate` and mmap write semantics differ).
The engine uses `core_affinity` to pin the fold thread to CPU core 1, keeping it off core 0 which is typically used by the OS scheduler and the primary writer thread.
---
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.
---
*Built by [TruthLinked Labs](https://truthlinked.org) for the TruthLinked validator network.*