The Swactor Datastore is a distributed personal file/blob storage protocol for small trusted clusters (laptop, phone, browser). It provides content-hash-first addressing with immutable content-addressed objects, replicated via a Kademlia-based metadata DHT.
### Design Principles
- **Content-hash-first addressing** — every object is identified by `blake3(blob_bytes)`. This is the primary key for all operations.
- **Immutable content-addressed objects** — content hashes are unique identifiers. There are no write conflicts by construction.
- **Names are metadata** — optional flat strings attached to objects, not keys. Multiple objects can share a name; distinguished by content hash.
- **Separation of data and metadata** — chunks are large opaque blobs; metadata is small, gossiped, and queryable.
- **Crash-safe** — fsync before acknowledge on all writes.
- **Actor-based** — three actor types coordinate via message passing within the swactor runtime.
- **Transport-agnostic** — protocol messages defined as `NetworkMessage` types; MVP uses iroh (QUIC + NAT hole-punch + encryption).
- **Pluggable storage** — `StorageBackend` trait abstracts I/O for filesystem (MVP), IndexedDB (browser), etc.
## 2. Terminology
| Term | Definition |
|------|-----------|
| **Object** | Content-addressed blob identified by `blake3(blob_bytes)`. May carry an optional human-readable name as metadata. |
| **Blob** | The raw byte content of an object. |
| **Chunk** | A fixed-size (1 MB default) slice of a blob, identified by its blake3 content hash. |
| **Manifest** | An ordered list of `ChunkRef`s describing how to reassemble an object from chunks. Stored under the object's content hash. |
| **ContentHash** | 32-byte blake3 digest. Primary identifier for blobs and DHT key. |
| **DHT overlay** | A Kademlia distributed hash table for object metadata, separate from the actor directory DHT. Keys are `blake3(blob_bytes)`. |
| **StorageBackend** | Trait abstracting chunk and manifest I/O for pluggable backends (filesystem, IndexedDB, etc.). |
| **Node** | A device running the swactor runtime with a datastore actor set (BlobStoreActor + MetadataActor). |
## 3. Data Model
### 3.1 ContentHash
```
ContentHash = blake3(data)[0..32] // 32 bytes
```
- **Hashing algorithm:** blake3 — 2-3x faster than sha256, tree-hashable (parallel hashing of large chunks), same 32-byte output. Supports streaming hashing for large blobs via `blake3::Hasher`.
- **Display:** first 8 bytes as hex + ellipsis (e.g. `a1b2c3d4e5f6a7b8…`).
- **XOR distance:** bitwise XOR of the 32-byte arrays, used for Kademlia routing in the metadata DHT.
created_at: u64, // Wall-clock creation time (informational)
}
```
No conflict resolution is needed — content hashes are unique identifiers. Storing the same blob twice is a no-op (same content hash). Different blobs always have different content hashes.
### 3.3 ObjectManifest
```
ObjectManifest {
content_hash: ContentHash, // blake3(entire_blob) — NOT the hash of this manifest
chunks: Vec<ChunkRef>, // Ordered list of chunks
total_size: u64, // Total object size in bytes
chunk_size: u32, // Fixed chunk size used (e.g. 1MB)
content_type: Option<String>, // MIME type
}
ChunkRef {
hash: ContentHash, // Content hash of chunk data
offset: u64, // Byte offset in original object
size: u32, // Actual size (last chunk may be smaller)
}
```
The `content_hash` field is `blake3(entire_blob)`, computed via a streaming hasher alongside chunking. The manifest is stored and looked up using this content hash as the key.
### 3.4 Storage Backend
The `StorageBackend` trait abstracts all chunk and manifest I/O:
> **Status:** Types and routing table logic exist in the `distribution` crate. `MetadataActor` has a dissemination queue (`enqueue`/`take_pending`) and peer-to-peer replication via `SetPeers` + `DisseminateTick`. Verified in local multi-node simulation. Full Kademlia iterative lookup (FIND_VALUE with α-parallel queries) is not yet implemented — dissemination is epidemic/gossip-style.
### 5.1 Overlay Design
The metadata DHT is a **separate Kademlia overlay** from the actor directory. It stores `ObjectEntry` records keyed by `blake3(blob_bytes)` — the content hash of the entire blob.
This separation ensures:
- Object metadata routing doesn't interfere with actor discovery.
- Different replication factors can be used (objects may be stored on fewer nodes).
- The DHT can be independently tuned for the metadata workload.
### 5.2 Key Mapping
```
DHT key = blake3(blob_bytes) = entry.content_hash
```
### 5.3 Store Flow
When storing object metadata:
1. Use `key = entry.content_hash`.
2. Find the `k` closest nodes to `key` in the metadata DHT routing table.
3. Send `StoreObjectRequest { entry }` to each of the `k` closest nodes.
### 5.4 Lookup Flow
When looking up object metadata:
1. Send `FindObjectRequest { content_hash }` to the `α` closest known nodes.
2. Each node responds with either `Found(ObjectEntry)` or `Closer(Vec<(NodeId, SocketAddr)>)`.
3. Continue querying closer nodes until convergence.
No merge step is needed — content hashes are unique identifiers.
## 6. Protocol Flows
### 6.1 PUT — Store an Object
```
User MetadataActor BlobStoreActor
│ │ │
│─── PutObject ────────────>│ │
│ │ │
│ │ (chunk the file, │
│ │ stream blake3 hash) │
│ │ │
│ │─── WriteChunk ────────>│
│ │<── ChunkStored ────────│ (repeat for each chunk)
> **Status:**`TransferActor` state machine is functional and stores received chunks to the local `BlobStoreActor`. Chunks must be fed externally (via `ChunkReceived` messages). Automatic chunk pulling from remote nodes is not yet implemented — the test harness or a future network adapter plays the "pull" role. Verified in multi-node simulation.
```
User MetadataActor TransferActor Remote BlobStore
│ │ │ │
│─ GetObject ──>│ │ │
│ {content_hash}│ │ │
│ │ (not in local │ │
│ │ index; DHT │ │
│ │ lookup) │ │
│ │ │ │
│ │─ StartDownload ───>│ │
│ │ │ │
│ │ │── GetChunkRequest ─>│
│ │ │<─ GetChunkResponse ─│
│ │ │ │
│ │ │ (repeat for each │
│ │ │ chunk) │
│ │ │ │
│ │<─ TransferComplete │ │
│ │ │ │
│<── GetOk ────│ │ (stops self) │
```
### 6.4 DELETE — Remove an Object
```
User MetadataActor
│ │
│─── DeleteObject ─────────>│
│ {content_hash} │
│ │
│ │ (remove from local )
│ │ (index, best-effort )
│ │ (notify DHT peers )
│ │
│<── DeleteOk │
│ {content_hash} ────────│
```
Chunk data is **not** immediately deleted. Unreferenced chunks are cleaned up during GC sweeps (see Section 10).
### 6.5 LIST — List Objects (Local)
```
User MetadataActor
│ │
│─── ListLocal ────────────>│
│ {name_filter} │
│ │
│ │ (filter local index )
│ │ (by name substring )
│ │
│<── ListOk { entries } ───│
```
### 6.6 LIST — List Objects (Swarm-Wide)
> **Status:**`ListSwarm` currently delegates to `ListLocal` (returns local entries only). Fan-out to peer MetadataActors is not yet wired. Swarm-wide listing is verified in simulation by querying each node and merging results in the test harness.
```
User MetadataActor Remote MetadataActors
│ │ │
│─ ListSwarm ──>│ │
│ {name_filter} │ │
│ │── ListObjectsRequest ─>│ (fan-out to all known
│ │<─ ListObjectsResponse ─│ alive nodes)
│ │ │
│ │ (merge all results, │
│ │ deduplicate by │
│ │ content hash) │
│ │ │
│<── ListOk ───│ │
```
## 7. Actor Architecture
> **Status:** All three actor types are fully implemented and tested. `DatastoreNode` coordinator routes commands to internal actors. 83+ tests across 8 test files verify single-node operations. Multi-node dissemination and cross-node transfers verified in simulation.
### 7.1 BlobStoreActor
**Responsibility:** Chunk and manifest I/O via `StorageBackend` trait.
- **State:**`Box<dyn StorageBackend>`
- **Lifecycle:** Long-lived, one per node.
- **Guarantees:** Delegates to backend; filesystem backend fsyncs before acknowledging.
**Message types:** `BlobStoreMsg` (see `messages.rs`)
**Message types:** `TransferMsg` (see `messages.rs`)
## 8. Wire Protocol
> **Status:** All message types are defined with `NetworkMessage` implementations and stable type tags. Serialization is JSON (serde). No transport integration yet — messages are passed directly via actor addresses in simulation.
### 8.1 Message Types
All inter-node messages implement `NetworkMessage` with a stable `type_tag()`:
`FindObjectRequest` contains a `content_hash` field (the `blake3(blob_bytes)` key).
### 8.2 Serialization
MVP: serde JSON for all messages. Binary format (bincode or msgpack) planned for later to reduce overhead, especially for `GetChunkResponse` which carries large payloads.
### 8.3 Framing
Messages are framed over iroh QUIC streams:
- Each request/response pair uses a single bidirectional stream.
Names are **optional flat strings** — human-readable labels attached to objects as metadata.
- Names are not keys. The content hash is the only primary identifier.
- Multiple objects can share the same name. They are distinguished by content hash.
- Names are simple strings (e.g. `"vacation.jpg"`, `"backup-2024-01"`). No path hierarchy, no separators enforced.
- No conflict resolution is needed — different content always produces different content hashes.
## 10. Garbage Collection
> **Status:** Fully implemented. `MetadataActor::gc_tick()` builds a referenced chunk set from all local manifests and sends `GcUnreferenced` to `BlobStoreActor`. Verified with 6 GC-specific tests including deduplication safety, interval gating, and empty-store edge case.
### 10.1 Entry Removal
Deleting an object:
1. Remove the `ObjectEntry` from the local index.
2. Best-effort notify DHT peers to remove their replicas.
3. Remove the local manifest.
### 10.2 Chunk Reference Counting
Unreferenced chunk cleanup:
1. Build a referenced set: union of all chunk hashes from all local manifest entries.
2. Send `BlobStoreMsg::GcUnreferenced { referenced }` to the BlobStoreActor.
3. BlobStoreActor diffs its chunk list against the referenced set and deletes unreferenced chunks.
**Safety:** A chunk may be referenced by multiple objects (deduplication). Only delete when zero references remain.
### 10.3 GC Schedule
- `MetadataActor` runs `gc_tick()` every tick. Actual GC sweep happens every `gc_interval` ticks (default: 1000).
- Chunk GC is triggered less frequently (order of minutes) to avoid overhead.
## 11. Failure Modes
### 11.1 Node Offline
- **Metadata persists** in the DHT (replicated to k-closest nodes). Lookups succeed as long as any replica is alive.
- **Chunk fetches fail** if the only copy is on the offline node. The TransferActor retries once, then reports failure.
- **Recovery:** When the node comes back, its metadata is re-disseminated (anti-entropy).
### 11.2 Transfer Interrupted
- **Partial state:** Some chunks may be written to the local BlobStoreActor before the transfer fails.
- **Cleanup:** Partially downloaded chunks are not harmful — they're content-addressed and may be useful for future downloads. Unreferenced chunks are cleaned up by GC.
- **Retry:** The user can retry the GET, and only missing chunks need to be fetched (future optimization).
### 11.3 DHT Inconsistency
- **Stale metadata:** A node may serve an outdated ObjectEntry. Anti-entropy dissemination ensures replicas converge.
- Content addressing eliminates write conflicts — storing the same content hash twice is idempotent.
### 11.4 Disk Full
- `StorageBackend::write_chunk` fails with an I/O error, which is propagated back to the requester as `DatastoreResponse::Error`.
- No partial writes — fsync ensures atomicity (filesystem backend).
## 12. CLI Interface
> **Status:** Command types defined in `src/cli.rs`. Parser, dispatcher, and `[[bin]]` target not yet implemented. Planned for a follow-up session.
```
swactor-store put <local-path> [--name <label>] [--tag key=value...]
Store a local file as a distributed object.
Returns the content hash of the stored object.
--name sets an optional human-readable label.
swactor-store get <content-hash>[@<node>] [--output <local-path>]
Retrieve an object by content hash. Fetches from the specified node or discovers via DHT.
--output defaults to the object's name (if set) in the current directory.
swactor-store delete <content-hash>
Remove an object from the local index and notify DHT peers.
swactor-store list [--name <substring>] [--node <node-name>] [--all]
List objects. --name filters by name substring. --all queries all nodes (swarm-wide). Default is local.
swactor-store status
Show node info: identity, chunk count, storage usage.