Network Protocol
Dits uses a custom protocol over QUIC for efficient, resumable transfers of large datasets with delta synchronization.
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