Architecture
Network Protocol
A design for a custom protocol over QUIC for efficient, resumable transfers of large datasets with delta synchronization.
Design — not yet wired into push/pull
This page describes the planned wire protocol. A QUIC engine exists today as a library plus a standalone demo, but it is not yet wired into
push, pull, or fetch— those commands do not move data over the network yet. The message formats and flows below are the target design, not shipped behavior. See the roadmap.Transport Layer
Dits uses QUIC (via the quinn crate) as its primary transport:
Why QUIC?
- Multiplexing: Multiple streams without head-of-line blocking
- 0-RTT: Faster connection establishment for repeat connections
- Connection migration: Handles network changes gracefully
- Built-in encryption: TLS 1.3 by default
- Better congestion control: Designed for modern networks
Connection Setup:
Client Server
| |
|-------- QUIC Handshake --------→ |
|←------- QUIC Handshake --------- |
| |
|------ Authentication Frame ----→ |
|←----- Auth Result Frame -------- |
| |
| (Multiple bidirectional streams) |
|←------------------------------- →|Protocol Messages
All messages are serialized using MessagePack for compact binary representation:
| Message Type | Direction | Purpose |
|---|---|---|
| AUTH | C → S | Client authentication |
| LIST_REFS | C → S | Get remote references |
| HAVE | C ↔ S | Advertise owned chunks |
| WANT | C → S | Request specific chunks |
| CHUNK | C ↔ S | Transfer chunk data |
| OBJECT | C ↔ S | Transfer commits/trees/assets |
| UPDATE_REF | C → S | Update a reference (push) |
| ACK | S → C | Acknowledge receipt |
| ERROR | S → C | Error response |
Message Formats
Authentication
message Auth {
// Authentication method
method: AuthMethod,
// Credentials based on method
credentials: Credentials,
// Client capabilities
capabilities: Vec<String>,
}
enum AuthMethod {
Token, // JWT bearer token
SSH, // SSH key signature
Password, // Username/password (discouraged)
}
message AuthResult {
success: bool,
user_id: Option<String>,
permissions: Vec<Permission>,
error: Option<String>,
}Reference Negotiation
message ListRefsRequest {
// Optional prefix filter
prefix: Option<String>,
}
message ListRefsResponse {
refs: Vec<RefInfo>,
}
message RefInfo {
name: String, // e.g., "refs/heads/main"
hash: [u8; 32], // Commit hash
peeled: Option<[u8; 32]>, // For annotated tags
}Chunk Negotiation
// Client advertises what it has
message HaveChunks {
// List of chunk hashes client has
chunks: Vec<[u8; 32]>,
// Whether this is a complete list
complete: bool,
}
// Client requests what it needs
message WantChunks {
// List of chunk hashes needed
chunks: Vec<[u8; 32]>,
// Priority hints
priority: Priority,
}
enum Priority {
Low, // Background fetch
Normal, // Standard fetch
High, // User is waiting
Urgent, // Blocking operation
}Chunk Transfer
message ChunkData {
// Chunk hash (for verification)
hash: [u8; 32],
// Compression used
compression: Compression,
// The data (compressed if applicable)
data: Vec<u8>,
// Sequence number (for ordering)
sequence: u64,
}
// Server acknowledges chunks
message ChunkAck {
// Hashes of successfully received chunks
received: Vec<[u8; 32]>,
// Hashes of chunks to resend
resend: Vec<[u8; 32]>,
}Fetch Protocol
Fetch Flow:
Client Server
| |
|-------- LIST_REFS -------------→ |
|←------- refs list --------------- |
| |
|-------- WANT commits ----------→ |
|←------- commit objects --------- |
| |
|-------- WANT trees ------------→ |
|←------- tree objects ----------- |
| |
|-------- WANT assets -----------→ |
|←------- asset manifests -------- |
| |
|-------- HAVE chunks -----------→ |
|←------- CHUNK (delta) ---------- |
|←------- CHUNK (delta) ---------- |
|←------- CHUNK (delta) ---------- |
|-------- ACK -------------------→ |
| |
|-------- DONE ------------------→ |Push Protocol
Push Flow:
Client Server
| |
|-------- LIST_REFS -------------→ |
|←------- refs list --------------- |
| |
| (Client computes delta) |
| |
|-------- HAVE chunks -----------→ |
|←------- WANT chunks ------------ |
| |
|-------- CHUNK -----------------→ |
|-------- CHUNK -----------------→ |
|-------- CHUNK -----------------→ |
|←------- ACK -------------------- |
| |
|-------- OBJECT (assets) -------→ |
|-------- OBJECT (trees) --------→ |
|-------- OBJECT (commits) ------→ |
| |
|-------- UPDATE_REF ------------→ |
|←------- ACK/ERROR -------------- |Parallel Streams
QUIC allows multiple streams per connection. Dits uses this for parallel chunk transfer:
// Stream allocation
Stream 0: Control messages (AUTH, LIST_REFS, UPDATE_REF)
Stream 1: Object transfer (commits, trees, assets)
Stream 2-N: Parallel chunk transfer
// Example: 8 parallel chunk streams
fn transfer_chunks(chunks: Vec<Chunk>, connection: &Connection) {
let streams: Vec<_> = (0..8)
.map(|_| connection.open_bi())
.collect();
// Distribute chunks across streams
for (i, chunk) in chunks.into_iter().enumerate() {
let stream = &streams[i % 8];
stream.send(ChunkData::from(chunk)).await?;
}
}Resumable Transfers
Transfers can be resumed after interruption:
// Client tracks transfer state
struct TransferState {
// Unique transfer ID
id: Uuid,
// Chunks already confirmed
completed: HashSet<[u8; 32]>,
// Chunks in flight (sent but not ACKed)
pending: HashSet<[u8; 32]>,
// Chunks still to send
remaining: Vec<[u8; 32]>,
}
// Resume protocol
message ResumeRequest {
transfer_id: Uuid,
last_ack_sequence: u64,
}
message ResumeResponse {
// Server's view of completed chunks
completed: Vec<[u8; 32]>,
// Resume from this point
resume_from: u64,
}Bandwidth Adaptation
Dits adjusts transfer parameters based on network conditions:
struct BandwidthEstimator {
// Exponentially weighted moving average
estimated_bandwidth: f64,
// Current RTT
rtt: Duration,
// Packet loss rate
loss_rate: f64,
}
impl BandwidthEstimator {
fn update(&mut self, bytes_sent: u64, time_taken: Duration) {
let sample = bytes_sent as f64 / time_taken.as_secs_f64();
self.estimated_bandwidth =
0.8 * self.estimated_bandwidth + 0.2 * sample;
}
fn recommended_parallelism(&self) -> usize {
// More streams for high bandwidth, fewer for constrained
let base = (self.estimated_bandwidth / 10_000_000.0) as usize;
base.clamp(1, 16)
}
fn recommended_chunk_batch(&self) -> usize {
// Batch size based on bandwidth-delay product
let bdp = self.estimated_bandwidth * self.rtt.as_secs_f64();
(bdp / 1_000_000.0) as usize + 1
}
}REST API (Fallback)
For environments where QUIC is blocked, Dits supports HTTP/2 REST API:
Endpoints:
GET /api/v1/repos/{repo}/refs
GET /api/v1/repos/{repo}/objects/{hash}
POST /api/v1/repos/{repo}/objects
GET /api/v1/repos/{repo}/chunks/{hash}
POST /api/v1/repos/{repo}/chunks
PUT /api/v1/repos/{repo}/refs/{name}
// Chunked upload for large data
POST /api/v1/repos/{repo}/upload
Content-Type: application/octet-stream
X-Dits-Chunk-Hash: {hash}
X-Dits-Compression: zstd
// Batch operations
POST /api/v1/repos/{repo}/batch
Content-Type: application/json
{
"operations": [
{"op": "get_chunk", "hash": "..."},
{"op": "get_chunk", "hash": "..."},
]
}Protocol Selection
Dits automatically selects the best available protocol. QUIC is preferred for performance, but falls back to HTTPS if UDP is blocked.
Related Topics
- Data Structures - What gets transferred
- Remote Commands - Using push/pull/fetch
- Encryption - Securing transfers