7.9 KiB
7.9 KiB
ADR-0010: chronogrid_net — CRDT Delta Sync Protocol (Task 2.4)
Status: Accepted
Date: 2026-08-22
Task: 2.4 — Implement chronogrid_net: CRDT Delta Sync Protocol
Author: Autonomous Agent
Context
Task 2.4 required implementing the CRDT delta exchange protocol over libp2p in
crates/chronogrid_net/, building on the transport and discovery layers from
Tasks 2.2 and 2.3.
The implementation must satisfy:
VectorClock— per-entity HLC head-state tracking for delta computation.DeltaPacket— serializable, compressed, encrypted CRDT changeset container.SyncProtocol— libp2pNetworkBehaviourfor request/response vector-clock exchange.GossipsubBroadcast— real-time state-change notifications via Gossipsub.NetEvent— unified event enum forSwarmManager(Task 2.5) consumption.
Decision
New module: crates/chronogrid_net/src/sync.rs
| Type | Role |
|---|---|
VectorClock |
HashMap<entity_id, hlc_timestamp> — lexicographic HLC comparison for delta computation |
DeltaPacket |
Serializable CRDT changeset with entity routing metadata |
SyncCodec |
libp2p request_response::Codec — length-prefixed bincode framing |
SyncProtocol |
#[derive(NetworkBehaviour)] wrapping ReqResBehaviour<SyncCodec> |
GossipEvent |
Application-level gossip message (entity_type, entity_id, hlc_timestamp, origin_peer) |
GossipsubBroadcast |
Wraps libp2p::gossipsub::Behaviour with topic subscription and event parsing |
NetEvent |
Unified event enum: DeltasReceived, SyncComplete, SyncError, GossipReceived |
VectorClock Design
HashMap<String, String>mappingentity_id → hlc_timestamp.- HLC timestamps use lexicographic ordering (format:
<ms>-<logical>-<node_prefix>), which is equivalent to causal ordering — no numeric parsing required. update()returnsboolindicating whether the entry was actually advanced.missing_from(other)computes the set of entity IDs whereotheris ahead ofself.merge(other)takes the maximum timestamp per entity (LWW semantics).- Fully serializable via
bincodefor wire transmission inSyncRequest/SyncResponse.
DeltaPacket Wire Format
[ 12-byte random nonce ]
[ ChaCha20-Poly1305 ciphertext of:
LZ4(bincode(DeltaPacket { entity_type, entity_id, hlc_timestamp, changeset_bytes, sequence }))
]
- Serialization:
bincode(compact binary, no schema overhead). - Compression:
lz4_flexwith size-prepended format — Automerge changesets compress well (typically 60–80% reduction for structured JSON-like data). - Encryption: ChaCha20-Poly1305 AEAD with the 32-byte
SwarmKeyas the symmetric key. A fresh 12-byte random nonce per packet ensures ciphertext uniqueness even for identical plaintexts. - Authentication: The AEAD tag authenticates the ciphertext; tampering causes decryption to fail with an error (not silent corruption).
- AAD:
b"chronogrid-delta"as additional authenticated data to bind the ciphertext to the ChronoGrid protocol context.
SyncProtocol (NetworkBehaviour)
- Wraps
libp2p::request_response::Behaviour<SyncCodec>via#[derive(NetworkBehaviour)]. SyncCodecimplementslibp2p::request_response::Codecusing#[async_trait](required by libp2p-request-response 0.27.0 which usesasync_traitinternally).- Frame format: 4-byte big-endian u32 length prefix + bincode payload.
- Maximum frame size: 64 MiB (generous upper bound for large delta batches).
send_sync_request()/send_sync_response()delegate to the inner behaviour.encode_deltas()/decode_deltas()are static helpers for batch processing.
GossipsubBroadcast Design
- Subscribes to topic
chronogrid/state-changes/1on construction. - Message deduplication: SHA-256 of message data as
MessageId(content-addressed). ValidationMode::Strict— only signed messages from known peers are accepted.handle_behaviour_event()mapsGossipBehaviourEvent::Message→NetEvent::GossipReceived.- All other Gossipsub events (Subscribed, Unsubscribed, GossipsubNotSupported) are
logged at DEBUG level and produce no
NetEventoutput.
NetEvent enum
pub enum NetEvent {
DeltasReceived { from_peer: PeerId, deltas: Vec<DeltaPacket> },
SyncComplete { peer_id: PeerId },
SyncError { peer_id: PeerId, reason: String },
GossipReceived { entity_type, entity_id, hlc_timestamp, origin_peer: String },
}
SwarmManager (Task 2.5) broadcasts these events via a
tokio::sync::broadcast::Sender<NetEvent> channel to all application subscribers.
New dependencies added
| Crate | Version | Purpose |
|---|---|---|
bincode |
1 | Compact binary serialization for DeltaPacket |
lz4_flex |
0.11 | LZ4 compression for delta payloads |
chacha20poly1305 |
0.10 | ChaCha20-Poly1305 AEAD encryption |
generic-array |
0.14 | GenericArray for AEAD key/nonce types |
async-trait |
0.1 | Required by libp2p request_response Codec trait |
libp2p feature |
— | Added "request-response" feature flag |
Test Coverage
100 unit tests pass + 5 doc-tests pass across all modules:
| Module | Tests | Coverage highlights |
|---|---|---|
sync |
35 | VectorClock (11), DeltaPacket (8), SyncProtocol helpers (3), GossipEvent (2), NetEvent (11) |
peer_event |
10 | (from Task 2.3, unchanged) |
transport |
12 | (from Task 2.3, unchanged) |
discovery |
13 | (from Task 2.3, unchanged) |
identity |
9 | (from Task 2.2, unchanged) |
swarm_key |
16 | (from Task 2.2, unchanged) |
sync (integration) |
1 | test_vector_clock_drives_delta_selection — full encode/decode/merge cycle |
Key test scenarios
- VectorClock: insert, update (newer/older/equal),
missing_fromwith empty/partial/superset, merge, iter, bincode round-trip. - DeltaPacket: encode/decode round-trip, random nonce uniqueness, wrong-key rejection, truncated payload rejection, tampered ciphertext rejection (AEAD authentication), 1 MiB large changeset, bincode round-trip.
- SyncProtocol: batch encode/decode, empty batch, construction.
- GossipEvent: bincode round-trip, invalid bytes rejection.
- NetEvent: all 4 variants — predicates, Display, Clone.
- Integration: two-peer delta selection driven by VectorClock comparison, encode/decode pipeline, clock update after merge.
Consequences
SwarmManager(Task 2.5) can now importSyncProtocol,GossipsubBroadcast,VectorClock,DeltaPacket, andNetEventdirectly fromchronogrid_net::sync.- The
SyncProtocolbehaviour must be polled by the libp2pSwarmevent loop in Task 2.5;send_sync_request()should be called onSwarmEvent::ConnectionEstablished. GossipsubBroadcast.behaviourmust be included in the compositeNetworkBehaviourderive struct in Task 2.5.chronogrid_core::sync::SyncEngine(Task 2.6) receivesNetEvent::DeltasReceivedand callsDatabase::merge_crdt_doc()for eachDeltaPacket.- No breaking changes to existing modules (
identity,swarm_key,transport,discovery).