Protocol Version
1.0.0
Chunk Transfer Protocol
This document specifies Lowkey's production-ready P2P chunk transfer system introduced in v0.2.7.
Overview
The chunk transfer protocol enables reliable file piece delivery between peers using:
- DHT Provider Discovery - Locate peers that have specific chunks
- Request-Response Protocol - Direct unicast chunk requests
- CBOR Binary Encoding - Efficient serialization without base64 overhead
- Gossipsub Fallback - Legacy broadcast for edge cases
Why Request-Response?
Previous versions used Gossipsub broadcast for chunk requests:
BEFORE (broken):
Peer A -> Gossipsub Broadcast -> [ALL PEERS] -> Filter by "to" field -> Peer B
Problem: Mesh takes 10-30 seconds to form, messages lost before mesh ready
AFTER (reliable):
Peer A -> DHT Lookup (providers) -> Direct Request -> Peer B -> Direct Response
Fallback: Gossipsub broadcast if no providers found
Protocol Specification
Protocol Identifier
/lowkey/chunk/1.0.0
Message Framing
All messages use length-prefixed framing:
+------------------+-----------------------------+
| Length (4 bytes)| CBOR Payload |
| Big-endian u32 | (variable length) |
+------------------+-----------------------------+
ChunkRequest
Sent by seekers to request a specific chunk:
// CBOR Structure
{
chunk_id: String // BLAKE3 hash as 64 hex characters
}
// Size limit: 256 bytes
ChunkResponse
Sent by providers in response:
// CBOR Structure
{
found: bool, // true if chunk was found
data: bytes, // Raw binary chunk data (NOT base64)
error: String | null // Error message if not found
}
// Size limit: 300 KiB (chunk data + overhead)
Response Examples
| Scenario | found | data | error |
|---|---|---|---|
| Success | true |
<binary chunk> | null |
| Not found | false |
[] |
"chunk not in store" |
| Seed node | false |
[] |
"seed node does not store chunks" |
DHT Provider Records
When sharing a file, peers announce themselves as providers for each chunk in the Kademlia DHT.
Provider Record Structure
// Key: provider:{chunk_id}
// Value (JSON):
{
"providers": [
{
"peer_id": "12D3KooW...",
"timestamp": 1704672000 // Unix seconds
}
]
}
// TTL: 24 hours
// Maximum providers per chunk: 10 (LRU by timestamp)
Provider Announcement
// Called when sharing a file
for chunk_id in manifest.chunks {
dht.announce_provider(&chunk_id).await?;
}
Fetch Algorithm
async fn fetch_chunk(chunk_id: &str) -> Result<Vec<u8>> {
// 1. Check local cache
if local_cache.has(chunk_id) {
return local_cache.get(chunk_id);
}
// 2. Find providers via DHT
let providers = dht.find_providers(chunk_id).await?;
// 3. Try direct request to each provider (up to 3)
for provider in providers.iter().take(3) {
match dht.request_chunk(provider, chunk_id).await {
Ok(data) if blake3::hash(&data).hex() == chunk_id => {
local_cache.store(chunk_id, &data);
return Ok(data);
}
Err(e) => continue, // Try next provider
}
}
// 4. Fallback: Gossipsub broadcast
fetch_chunk_via_gossipsub(chunk_id).await
}
Gossipsub Fallback
When no providers are found or all direct requests fail:
async fn fetch_chunk_via_gossipsub(chunk_id: &str) -> Result<Vec<u8>> {
// Subscribe to response topic
subscribe("lowkey-chunk-resp");
// Broadcast request
publish("lowkey-chunk-req", {
"t": "req",
"from": my_peer_id,
"chunk": chunk_id
});
// Wait for response (filtered by 'to' field)
for message in receive_with_timeout(30s) {
if message["to"] == my_peer_id && message["chunk"] == chunk_id {
if message["ok"] {
return base64_decode(message["data_b64"]);
}
}
}
Err(ChunkNotFoundError)
}
Performance Characteristics
Latency
| Operation | Typical Time |
|---|---|
| DHT provider lookup | 50-200ms |
| Direct chunk request | 100-500ms |
| Gossipsub fallback | 5-30s (mesh formation) |
Bandwidth Comparison
| Encoding | Overhead |
|---|---|
| CBOR (Request-Response) | ~5% |
| JSON + base64 (Gossipsub) | ~38% |
Configuration
| Parameter | Value |
|---|---|
| Max concurrent chunk fetches | 8 |
| Request timeout | 30 seconds |
| Providers to try | 3 |
| Provider TTL | 24 hours |