Skip to main content
Documentation

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 TypeDirectionPurpose
AUTHC → SClient authentication
LIST_REFSC → SGet remote references
HAVEC ↔ SAdvertise owned chunks
WANTC → SRequest specific chunks
CHUNKC ↔ STransfer chunk data
OBJECTC ↔ STransfer commits/trees/assets
UPDATE_REFC → SUpdate a reference (push)
ACKS → CAcknowledge receipt
ERRORS → CError 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": "..."},
       ]
     }

Related Topics