How Xudanu stores and retrieves documents: content-addressed chunks, incremental checkpoints, and O(1) integrity verification.
Xudanu's storage engine was designed to solve four specific problems that emerge when a content-addressed hypertext system scales beyond a prototype.
Figure 1: Four requirements that shaped the storage design, unified by content-addressed chunking.
The key insight: Xudanu already uses BLAKE3 content fingerprints throughout the engine — for transclusion identity, content deduplication, and the ContentAddressIndex. Reusing the same fingerprint as the storage key creates a system where identity, caching, and persistence all speak the same language.
ent database) with no incremental checkpointing or content-addressed chunks. Xudanu's three-layer storage (ChunkStore + Manifest + WAL) is a modern replacement that would not have been practical in the C++ era.
The original storage mechanism wrote the entire server state to a single server.json file on checkpoint. This worked for prototyping but had three critical flaws that would prevent production use:
{
"works": [
{ "be_id": 1, "edition": {
"entries": [
/* 10,000 entries */
]
}},
/* 5,000 more works */
],
"clubs": /* ... */,
"links": /* ... */
}
manifest.json /* 12 KB index */ chunks/ 0a/ 0a3f...b2c1 /* entry chunk */ 0a7e...d4a8 /* entry chunk */ 3b/ 3bc9...1f07 /* root chunk */ /* 256 shard dirs max */
| Problem | Monolithic JSON | Chunked Storage |
|---|---|---|
| Checkpoint cost | O(n) — re-serialize everything | O(changed) — write only modified chunks |
| Crash safety | Partial write corrupts entire state | Atomic rename per chunk; manifest is separate |
| Deduplication | Same content stored N times | BLAKE3 hash = filename; identical content stored once |
| Memory usage | Must hold entire JSON in memory to parse | Load only needed chunks; LRU cache for hot data |
| Federation sync | Ship entire file to peers | Exchange individual chunks by hash |
BeId in the GrandMap. When that text is serialized to a chunk, it gets the same BLAKE3 hash regardless of which document triggered the write. Storage-level deduplication is free.
The storage system is organized in three layers, each with a distinct responsibility. The top layer is a lightweight JSON index; the middle layer handles serialization of document structures into binary chunks; the bottom layer is the content-addressed file store.
Figure 2: Three layers separate concerns — indexing, serialization, and physical storage.
ChunkStore is the foundation. It maps arbitrary byte sequences to files named by their BLAKE3 hash. The API is intentionally minimal:
pub struct ChunkStore {
base_dir: PathBuf,
cache: Mutex<Cache>, // LRU, 1024 entries
cache_hits: AtomicU64,
cache_misses: AtomicU64,
}
impl ChunkStore {
pub fn write_chunk(&self, data: &[u8]) -> Result<[u8; 32], ChunkError>;
pub fn read_chunk(&self, hash: &[u8; 32]) -> Result<Vec<u8>, ChunkError>;
pub fn chunk_exists(&self, hash: &[u8; 32]) -> bool;
pub fn verify_chunk(&self, hash: &[u8; 32]) -> Result<(), ChunkError>;
}
Figure 3: Write path — hash, check existence, atomic write, cache.
Every read_chunk checks the LRU cache first. On a cache hit, no disk I/O occurs. On a miss, the chunk is read from disk and its BLAKE3 hash is verified against the requested hash before returning. This means corruption is detected at read time, not just at verification time.
BLAKE3 serves double duty in Xudanu: it is both the content fingerprint used for transclusion identity and the storage key used for chunk addressing. This is not coincidental — it is a design decision that eliminates an entire class of mapping overhead.
Figure 4: BLAKE3 unifies content identity and storage addressing.
| Property | BLAKE3 | Why It Matters |
|---|---|---|
| Output size | 256 bits (32 bytes) | Collision probability < 2-128 — negligible for any dataset size |
| Speed | ~1 GB/s single-threaded | Hashing never becomes the bottleneck |
| Deterministic | Same input always produces same hash | Content identity is stable across restarts, peers, and checkpoints |
| Verification | Re-hash and compare | Integrity check is just: does blake3(file_contents) == filename? |
An Edition (Xudanu's document data structure) is an O-tree with entries at integer positions. Rather than serializing an entire edition as one blob, edition_chunks.rs splits it into a root chunk and multiple entry chunks, each holding up to 256 entries.
Figure 5: An Edition with 300 entries becomes 3 chunks: 1 root + 2 entry chunks. Entry chunks hold up to 256 entries each.
A Work (document) contains its current edition plus a revision history. Each revision is stored as its own edition chunk tree. The WorkChunkRef is the manifest's pointer to a work's data:
pub struct WorkChunkRef {
be_id: BeId,
owner: Option<BeId>,
revision_count: u64,
current_root: EditionChunkRef, // current version
history: BTreeMap<u64, EditionChunkRef>, // past versions
read_club: Option<BeId>,
edit_club: Option<BeId>,
sponsors: Vec<BeId>,
}
Revisions are loaded lazily. work_from_chunks_current() reads only the current edition. work_load_revision() loads a specific revision by number. This means opening a document with 200 revisions reads 1 edition, not 201.
The root chunk of an edition may also contain an optional provenance_hash pointing to a separate ProvenanceChunk. This chunk stores SpanProvenance data that tracks where each span of content originally came from — essential for transclusion tracking. When content is transcluded from one document into another, the provenance record preserves the source document's identity and span location, enabling correct attribution and back-references even after the source document is modified.
The manifest (manifest.json) is the only JSON file in the system. It contains references (not data) to all works, clubs, links, and administrative state. It is small by design:
pub struct Manifest {
format_version: u32, // currently version 3
created_at: String, // ISO 8601 timestamp
server_version: String, // cargo package version
checksum: String, // SHA-256 of content
grand_map_id_counter: BeId,
session_counter: u64,
operation_counter: u64,
system_clubs: SystemClubs,
works: Vec<(BeId, WorkChunkRef)>, // references to chunk trees
clubs: Vec<ClubChunkRef>,
standalone_editions: Vec<StandaloneEditionChunkRef>,
links: Vec<LinkEntry>,
link_counter: BeId,
admin: AdminEntry,
reconcile_store: ReconcileStore, // federation reconciliation
reconcile_counter: u64,
federation: Option<FederationSnapshot>,
content_address: Option<ContentAddressIndex>,
blob_metas: Vec<BlobMetaEntry>, // binary blob metadata
key_history: Option<KeyHistoryEntry>, // server key rotation history
}
Each club is stored as its own chunk tree. The ClubChunkRef in the manifest captures all club metadata:
pub struct ClubChunkRef {
be_id: BeId,
name: Option<String>,
signature_club: Option<BeId>,
work_root: WorkChunkRef,
default_read_club: Option<BeId>,
default_edit_club: Option<BeId>,
is_personal: bool, // personal identity clubs
display_name: Option<String>, // human-readable name
credential: Option<Credential>, // password/boo auth
encrypted_signing_key: Option<EncryptedSigningKey>,
members: Vec<BeId>,
sponsored_works: Vec<BeId>,
}
work_root: WorkChunkRef pointing to a chunk tree for club-level documents (member lists, permissions). This means clubs are self-contained — loading a club reads its work root plus member data from a single chunk tree.
Figure 6: The manifest references chunks by BLAKE3 hash. Chunk data lives separately.
The manifest includes an SHA-256 checksum computed over its own content (with timestamp and version fields zeroed). On load, the checksum is recalculated and compared. If they don't match, the manifest is rejected with a ChecksumMismatch error. This catches accidental edits, partial writes, and bit-level corruption.
write manifest.tmp → rename manifest.tmp to manifest. On POSIX systems, rename is atomic on the same filesystem, so a crash during write leaves either the old or new manifest — never a partial file.
The most significant performance benefit of chunked storage is incremental checkpoints. When a user edits a document:
Figure 7: With 10,000 documents and 1 edit, the old system writes all 10K. The new system writes the changed edition's chunks plus the manifest.
The checkpoint path in server.rs (checkpoint_to_store()) follows this sequence:
chunk_ref exists, serialize dirty onesdirty_clubs set and cached ref exists"Checkpoint saved in Xms (dirty: W/C/E works/clubs/editions)"Incremental checkpoints rely on precise dirty tracking to avoid re-serializing unchanged data. Each entity type uses a different strategy:
Figure 8: Works use cached refs as implicit dirty flags. Clubs use an explicit HashSet for precise tracking.
ChunkStore::write_chunk is idempotent (same data = same hash = no disk write if chunk exists), unchanged works produce no I/O even if the checkpoint code iterates over them. The dirty tracking is an optimization that avoids the serialization cost, not just the disk I/O.
Xudanu provides a built-in verification tool: xudanu-server verify <data-dir>. This command performs a comprehensive integrity check across all stored data.
Figure 9: Five-stage verification catches corruption, missing data, and storage leaks.
$ xudanu-server verify /tmp/xudanu-data Verification report: Chunks: 1,247 total, 1,247 verified Works: 50 ok, 0 failed Clubs: 3 ok, 0 failed Standalone editions: 12 ok, 0 failed Status: OK
The VerifyReport tracks: chunks verified, chunks corrupt (hash mismatch), chunks missing (referenced but not on disk), chunks orphaned (on disk but not referenced), deserialization errors, and per-type (work/club/edition) success/failure counts. A single failure in any category makes the overall status ISSUES FOUND.
After every checkpoint, the server automatically runs gc_orphaned_chunks() to reclaim disk space occupied by chunks no longer referenced by any work, club, or standalone edition. This prevents unbounded growth as documents are edited and old chunk trees are superseded.
Figure 10: Garbage collection reclaims disk space by removing chunks no longer reachable from the manifest.
The GC process is safe because it runs after the manifest has been atomically written. Any chunk that was referenced by the previous manifest but is no longer referenced by the new one is guaranteed to be an orphan — the new manifest's references are the single source of truth.
Content-addressed storage is a natural fit for federation. When two Xudanu servers synchronize, they exchange content at the chunk level. Because chunk identity is determined by content (BLAKE3 hash), peers can:
Figure 11: Federation sync exchanges only missing chunks. Shared content (same hash) is never sent twice.
The chunk-based federation protocol works in three steps:
| Step | Action | Data Transferred |
|---|---|---|
| 1. Manifest exchange | Peers share their manifests | Small JSON (KB) |
| 2. Want/have negotiation | Compare chunk hash sets | Just 32-byte hashes |
| 3. Chunk transfer | Send only missing chunks | Binary chunk data |
The storage system was designed for the access patterns that Xudanu actually exhibits: frequent writes to a small number of active documents, infrequent reads of historical revisions, and periodic full checkpoints.
| Operation | Complexity | Notes |
|---|---|---|
| Write chunk | O(1) | Hash data, check exists, atomic write. Same data = no-op. |
| Read chunk (cached) | O(1) | LRU lookup, no disk I/O. Cache holds 1,024 entries. |
| Read chunk (cold) | O(chunk_size) | Disk read + BLAKE3 verify. One file read per chunk. |
| Checkpoint (incremental) | O(changed) | Only modified chunks written. Unchanged works: zero I/O. |
| Checkpoint (full) | O(total) | All chunks serialized. But idempotent writes skip existing chunks. |
| Verify single chunk | O(chunk_size) | Read + re-hash + compare to filename. |
| Verify entire store | O(total) | Walk all chunks. Manifest + all chunk files. |
| Load work (current) | O(1 edition) | Only current_root chunk tree is read. History stays on disk. |
| Load revision N | O(1 edition) | BTreeMap lookup → one edition chunk tree. No scanning. |
| Deduplication | O(1) | Automatic: same bytes → same hash → same file. No dedup pass needed. |
The 1,024-entry LRU cache is particularly effective for Xudanu's workload. A user typically edits a handful of documents at a time. Each edit re-reads the same edition chunks and writes new ones for changed entries. The hot set (current editions of active documents) fits comfortably in 1,024 entries.
Figure 12: LRU cache with 1,024 entries covers the active working set of most sessions.
data-dir/ manifest.json # Lightweight index (SHA-256 checksummed) server.key # Server identity key (encrypted or plaintext) blobs/ # Binary blobs (images, attachments) {blake3-hex} # Content-addressed binary data chunks/ # Edition and work data 00/ # Shard 00 {64-char-blake3-hex} # One file per unique chunk 01/ # Shard 01 ... ff/ # Shard ff ... security.log # Chained security audit log security.log.seed # Chain verification seed
| Command | Purpose |
|---|---|
xudanu-server init <dir> |
Create data directory, generate server key, write empty manifest |
xudanu-server run [addr] [dir] |
Start server. Restores from manifest + chunks. Checkpoints on Ctrl-C. |
xudanu-server verify <dir> |
Full integrity check: manifest checksum, chunk hashes, deserialization, orphan detection |
xudanu-server rebuild-manifest <dir> |
Reconstruct manifest from chunk files (disaster recovery) |
xudanu-server verify-security-log <dir> |
Verify security log chain integrity (hash chain from seed) |
Binary blobs (images, attachments, and other non-text data) are stored in the blobs/ directory using the same content-addressed scheme as edition chunks. Each blob's filename is its BLAKE3 hash, ensuring automatic deduplication.
The manifest tracks blob metadata via BlobMetaEntry records:
pub struct BlobMetaEntry {
content_hash: [u8; 32], // BLAKE3 of blob data
byte_size: u64, // original file size
mime_type: String, // e.g. "image/png"
preview_hash: Option<[u8; 32]>,// thumbnail/preview BLAKE3
metadata: HashMap<String, String>, // extensible metadata
}
The manifest stores a KeyHistoryEntry that enables key rotation verification across server restarts:
pub struct KeyHistoryEntry {
server_id: BeId,
entries: Vec<KeyHistoryRecord>,
rotation_proofs: Vec<SignedProof>,
current_key_id: String,
}
Each key rotation generates a signed proof that the old key authorized the new key. On startup, the server verifies the entire chain from the initial key to the current key. This prevents an attacker who compromises an old key from forging subsequent rotations.
Chunk writes are atomic (tmp + rename). Manifest write is atomic. At worst, the previous checkpoint's data is intact. Individual chunks are either fully written or not present. The verify command identifies any gaps.
Each chunk is verified by filename = BLAKE3(contents). A corrupted chunk produces a HashMismatch error. The verify report lists all corrupt chunks by hash. Because chunks are small and independent, recovery can target individual files.
rebuild-manifest reconstructs the manifest from the chunk files on disk. Works and editions are rebuilt from their chunk trees. Not all metadata survives (admin grants, federation state), but document content is preserved.
Copy the entire data-dir/ to the new server. Because chunks are content-addressed, the new server can verify them independently. The server key (server.key) controls identity — copy it to preserve server ID, or generate a new one for a fresh identity.
Figure 13: Three guarantees provided by the storage architecture.
src/persist/chunk_store.rs (950 lines) — ChunkStore with LRU cache.
src/persist/edition_chunks.rs (508 lines) — Edition/Work serialization.
src/persist/manifest.rs (418 lines) — Manifest with checksums.
src/persist/verify.rs (422 lines) — Full-store verification.
src/persist/stress.rs (1,259 lines) — Performance stress tests at Fast/Medium/Heavy scales.
src/server/server.rs — checkpoint_to_store(), gc_orphaned_chunks(), dirty tracking.