State Chain Sync¶
State chain blocks are synchronised between peers using a dedicated libp2p stream protocol. This ensures all nodes converge on the same chain state, even if they were offline when blocks were published.
Sync protocol¶
| Property | Value |
|---|---|
| Protocol ID | /xe/statechain-sync/1.0.0 |
| Gossip topic | xe/statechain |
| Transport | libp2p stream (JSON over TCP) |
| Page size | 64 blocks per response |
| Max blocks per sync | 10,000 |
| Stream deadline | 60 seconds |
| Rate limit | 30-second cooldown per peer |
Message types¶
Request¶
A value of -1 means the client has no blocks (not even genesis) and wants everything from index 1 onward. Genesis (index 0) is never sent over sync -- it is configured locally.
Response¶
Responses are paginated. If HasMore is true, the client should expect additional response pages on the same stream.
Sync flow¶
Client Node Server Node
│ │
│ open stream │
│───────────────────────────────────►│
│ │
│ syncRequest{TipIndex: 5} │
│───────────────────────────────────►│
│ close write │
│ │
│ syncResponse{Blocks:[6..69], │
│ HasMore: true} │
│◄───────────────────────────────────│
│ │
│ syncResponse{Blocks:[70..100], │
│ HasMore: false} │
│◄───────────────────────────────────│
│ │
│ stream closed │
- The client opens a stream to the server using the sync protocol ID.
- The client sends a
syncRequestwith its current tip index, then closes the write side. - The server reads the request and sends back all blocks after the client's tip, paginated in chunks of 64.
- Each page is a JSON-encoded
syncResponse. The last page hasHasMore: false. - The client applies each received block via
chain.AddBlock().
Trigger mechanisms¶
Sync is triggered in two ways:
On peer connection¶
When a new peer connects, the node automatically initiates an outbound sync:
New peer connected
│
▼
Rate limit check (30s cooldown)
│
▼
Open sync stream → send tip index → receive blocks
This ensures nodes catch up immediately when they join the network or reconnect after downtime.
Via gossip¶
New state chain blocks are broadcast to all peers via the xe/statechain gossip topic. When a node receives a gossip block, it calls chain.AddBlock() directly. If the block has a gap (e.g., the node missed earlier blocks), the add will fail, and the node will catch up on the next peer connection sync.
Rate limiting¶
Both inbound and outbound sync are rate-limited independently:
| Direction | Cooldown | Purpose |
|---|---|---|
| Inbound | 30 seconds per peer | Prevents a peer from flooding the server with sync requests |
| Outbound | 30 seconds per peer | Prevents redundant sync requests to the same peer |
The rate limiter automatically cleans up stale entries when checking for allowance.
Startup replay¶
On startup, the chain replays all stored blocks to rebuild the in-memory KV state:
NewChain()applies genesis ops.replay()reads all stored blocks (by index) and applies their ops in order.- After replay, the chain verifies that a valid
sys.dao_keysetexists. - Sync handlers are registered so the node can catch up from peers.
Deterministic rebuild
Because operations are pure key-value mutations applied in index order, every node that has the same blocks will arrive at the same KV state. There is no non-deterministic input.
Size limits¶
| Limit | Value | Description |
|---|---|---|
syncPageSize |
64 | Blocks per response page |
syncMaxBlocks |
10,000 | Maximum total blocks per sync session |
syncMaxRequestSize |
1,024 bytes | Maximum sync request payload |
syncMaxResponseSize |
10 MB | Maximum response page size |
syncStreamDeadline |
60 seconds | Stream timeout |
syncCooldown |
30 seconds | Per-peer rate limit interval |
Error handling¶
- Block validation failures during sync are logged but do not abort the sync. The node continues processing remaining blocks -- a single invalid block does not poison the session.
- Stream errors (timeout, disconnect) terminate the sync. The node will retry on the next peer connection.
- Rate-limited requests are silently dropped by the server.