SyncWave
Bootstrap a fresh node in seconds — download a verified state snapshot at a checkpoint block, prove it locally, resume from there.
Overview
SyncWave is Sahyadri's fast-bootstrap primitive. A brand-new node does not need to replay the chain from genesis. It downloads a snapshot of account state at a recent checkpoint block, verifies the snapshot cryptographically against that block's header, loads it into local stores, and resumes normal block sync from the checkpoint forward.
The snapshot is untrusted data. Any peer can serve it. The receiving node rebuilds the SMT root from the transmitted nodes and compares it against the checkpoint header's account_commitment. If they match, the snapshot is genuine. If they don't, the peer is lying — drop it and try another.
The Bootstrap Problem
Every blockchain faces the same cold-start cost. A new node must reconstruct the current state — every balance, every account — from scratch. Two naive approaches exist, and both fail:
| Approach | Cost | Why it fails |
|---|---|---|
| Replay from genesis | Hours to days | Every block must be re-executed. On a growing chain, this window never closes. |
| Trust a peer's state | Seconds | The peer can lie about balances, omit accounts, or serve a stale fork. No verification. |
SyncWave offers a third path: download untrusted state, verify it cryptographically against a header. The peer's role collapses to that of a dumb pipe. The verification is local, cheap, and requires only the checkpoint header the client already trusts.
This is the same trust model as state proofs, scaled up. A state proof proves one account against a root. A SyncWave snapshot proves the entire tree against the same root.
How It Works
SyncWaveMetadata — a few hundred bytes: block hash, height, SMT root, total node count, chunk count.N chunks of chunk_size entries each. Each chunk carries SMT nodes and account states. The client downloads them sequentially, any peer, any order.verify(). Every SMT node is content-addressed — its hash must equal its declared key. The walk from account_root must resolve completely, without dangling references. If it does, the snapshot equals the header's committed root.The Snapshot Structure
A snapshot is a self-describing bundle: metadata plus the entire reachable state of the SMT at the checkpoint block.
pub struct SyncWaveSnapshot {
pub metadata: SyncWaveMetadata,
pub smt_nodes: Vec<(H256, SyncWaveNode)>,
pub account_states: Vec<(Hash, SyncWaveState)>,
}
pub struct SyncWaveMetadata {
pub block_hash: Hash, // checkpoint block
pub block_height: u64, // blue score
pub account_root: Hash, // == header.account_commitment
pub total_smt_nodes: u64,
pub total_accounts: u64,
pub chunk_size: u32, // entries per chunk
pub total_chunks: u32,
}
pub enum SyncWaveNode {
Leaf { key: H256, value: H256 },
Branch { left: H256, right: H256 },
}
pub struct SyncWaveState {
pub balance: u64,
pub recent_flashes: Vec<SyncWaveFlashEntry>,
}Every SMT node is stored under its own content-address. The address of a leaf is SHA3("SAHYADRI_SMT_LEAF_V1" || key || value); a branch is SHA3("SAHYADRI_SMT_BRANCH_V1" || left || right). This domain separation is what makes the tree canonical and the snapshot self-verifying.
Account states are stored under their own content-hash — the value of the SMT leaf. The state hash is what a state proof resolves to; the snapshot just carries the states themselves.
Metadata-First Protocol
The client fetches metadata before committing to the download. This is deliberate: a snapshot can be hundreds of megabytes. Metadata tells the client how big the transfer will be, how many chunks, and — critically — the block hash whose header must be verified.
pub struct RpcSyncWaveMetadata {
pub block_hash: RpcHash,
pub block_height: u64,
pub account_root: RpcHash,
pub total_smt_nodes: u64,
pub total_accounts: u64,
pub chunk_size: u32,
pub total_chunks: u32,
}The client verifies the checkpoint header first: PoW is valid, the block is on the selected chain, the account commitment in the header equals the metadata's account_root. Only then does it begin downloading chunks. A malicious peer cannot lure the client into wasting bandwidth on a fake snapshot for a block the client would never accept.
Chunk Wire Format
Chunks are bincode-encoded for wire efficiency. The wire type is deliberately minimal — no re-encoding, no hex conversion. The bytes can be sliced, streamed, and reassembled without inspecting structure.
pub struct SyncWaveChunkWire {
pub chunk_index: u32,
pub total_chunks: u32,
pub smt_nodes: Vec<(H256, SyncWaveNode)>,
pub account_states: Vec<(Hash, SyncWaveState)>,
}The snapshot's full entry list is treated as a single sequence: SMT nodes first, then account states. Chunks are slices of this sequence. The client concatenates chunks in order and reconstructs the original two vectors.
The total_chunks field is repeated in every chunk as a sanity check. A client that receives a chunk claiming a different total than the metadata is talking to a peer with a different snapshot; it drops the connection and tries another.
Server-Side Export
On the serving node, SyncWaveExporter::export produces a snapshot for a given block. The work is a single DFS over the SMT, followed by a batched read of every reachable state.
pub fn export(&self, block_hash: Hash) -> SyncWaveResult<SyncWaveSnapshot> {
let header = self.headers_store.get_header(block_hash)?;
let root = hash_to_h256(header.account_commitment);
// 1. Walk the SMT from the committed root, collect every node.
let base = DbSmtNodeStoreBase::new(&self.smt_nodes_store);
let mut nodes: HashMap<H256, SyncWaveNode> = HashMap::new();
self.collect_smt_nodes(&base, root, &mut nodes)?;
// 2. For each leaf, fetch its AccountState from the state store.
let mut states: Vec<(Hash, SyncWaveState)> = Vec::new();
for node in nodes.values() {
if let SyncWaveNode::Leaf { value, .. } = node {
let state_hash = h256_to_hash(*value);
let state = self.account_states_store.get(state_hash)?;
states.push((state_hash, to_sync_state(&state)));
}
}
let total_smt_nodes = nodes.len() as u64;
let total_accounts = states.len() as u64;
let chunk_size = DEFAULT_CHUNK_SIZE;
let total_chunks = ((total_smt_nodes + total_accounts) as u32)
.div_ceil(chunk_size);
Ok(SyncWaveSnapshot {
metadata: SyncWaveMetadata {
block_hash,
block_height: header.blue_score,
account_root: header.account_commitment,
total_smt_nodes,
total_accounts,
chunk_size,
total_chunks,
},
smt_nodes: nodes.into_iter().collect(),
account_states: states,
})
}The export is read-only. It touches no persistent state, mutates nothing. Any archival node can serve snapshots for any historical block whose header it still has — the SMT is content-addressed, so old roots remain queryable as long as the nodes are retained.
The DFS memoizes visited hashes so shared subtrees are visited only once. In practice the effective depth is under 64, so even a tree with millions of accounts walks in milliseconds.
Client-Side Verification
Verification is entirely local. It makes no network calls. It requires only the assembled snapshot and the trusted root that came from the checkpoint header.
pub fn verify(snapshot: &SyncWaveSnapshot) -> SyncWaveResult<()> {
let root = hash_to_h256(snapshot.metadata.account_root);
// 1. Every node's content-address must equal its declared key.
let mut index: HashMap<H256, &SyncWaveNode> = HashMap::new();
for (h, node) in &snapshot.smt_nodes {
if node.hash() != *h {
return Err(SyncWaveError::NodeHashMismatch(*h));
}
index.insert(*h, node);
}
// 2. Walk from the root. Every reference must resolve.
let mut visited: HashMap<H256, ()> = HashMap::new();
Self::walk_and_check(root, &index, &mut visited)?;
// 3. The walk must visit exactly as many nodes as declared.
if visited.len() as u64 != snapshot.metadata.total_smt_nodes {
return Err(SyncWaveError::NodeCountMismatch {
declared: snapshot.metadata.total_smt_nodes,
actual: visited.len() as u64,
});
}
// 4. Every state's content-hash must match its key.
for (state_hash, state) in &snapshot.account_states {
if from_sync_state(state).content_hash() != *state_hash {
return Err(SyncWaveError::StateHashMismatch(*state_hash));
}
}
Ok(())
}Each check defends against a specific attack:
- Step 1 rejects a node whose contents don't hash to its declared address. An attacker cannot forge a leaf or branch without breaking SHA3-256.
- Step 2 rejects a snapshot that references a node it did not include. This is the "dangling reference" attack — omitting a subtree while keeping the root valid.
- Step 3 rejects a snapshot that declares more nodes than it carries. An attacker cannot pad the metadata to inflate the client's work.
- Step 4 rejects a state whose value doesn't match its own content-hash. This binds each account's balance to the leaf value that was committed in the tree.
If all four pass, the snapshot is the state at the checkpoint block — not a copy, not an approximation. There is exactly one tree that hashes to the committed root, and the client has reconstructed it.
Loading & Atomicity
Once verified, the snapshot is inserted into the local stores. The design goal is crash-safety: a node killed mid-load should not be left in a state that requires re-downloading the entire snapshot.
pub fn load(&self, snapshot: &SyncWaveSnapshot) -> SyncWaveResult<()> {
// SMT nodes — content-addressed, so writes are idempotent.
for (h, node) in &snapshot.smt_nodes {
self.smt_nodes_store.insert_sync(*h, into_smt_node(node))?;
}
// Account states — content-addressed by state hash.
for (state_hash, state) in &snapshot.account_states {
self.account_states_store.insert_sync(*state_hash, &from_sync_state(state))?;
}
// The root mapping — keyed by block hash, not content-addressed.
let mut batch = WriteBatch::default();
self.account_roots_store.insert_batch(
&mut batch,
snapshot.metadata.block_hash,
hash_to_h256(snapshot.metadata.account_root),
)?;
self.db.write(batch)?;
Ok(())
}Nodes and states are content-addressed. Writing the same (hash, value) pair twice is a no-op — the second write overwrites with identical bytes. If the node crashes halfway through, the SMT store holds a partial but self-consistent subset of the tree. Re-running the load completes it.
The root mapping is the only non-idempotent write. It is a single key-value insert, wrapped in a write batch so it either lands fully or not at all. If the checkpoint root was never persisted, the node treats its store as incomplete on restart and re-downloads.
Bootstrap Flow
Fetch Metadata
get_sync_wave_metadata() returns a few hundred bytes. The client now knows the checkpoint block hash, its height, and how many chunks to expect.
Verify the Checkpoint Header
Download the header for the checkpoint block, verify PoW and DAG ordering, extract account_commitment. This is the only trusted input. Everything after is cryptographic.
Download Chunks
Sequentially fetch download_sync_wave_chunk(hash, i) for i in 0..total_chunks. Chunks may come from any peer; each is self-contained.
Assemble & Verify
Concatenate chunks into a SyncWaveSnapshot, run verify(). Under a millisecond for typical trees. If verification fails, discard the download and retry with a different peer.
Atomic Load
Insert nodes and states into local stores. Content-addressed writes make this idempotent and crash-safe. The checkpoint root is persisted last, in a single write batch.
Sync from Checkpoint to Tip
The node now has the state at block N. It runs ordinary IBD from N to the current tip. Because the gap is small (a few minutes of blocks), this completes in seconds.
Node Goes Online
Full state, current tip, ready to serve RPC and P2P. Total elapsed time: seconds to low minutes, versus hours of full IBD from genesis.
RPC Endpoints
Two methods, both read-only, both available on any full node. No mining, no consensus interaction, no state mutation.
get_sync_wave_metadata(
block_hash: Option<Hash>, // defaults to current tip
) -> {
block_hash: Hash,
block_height: u64,
account_root: Hash,
total_smt_nodes: u64,
total_accounts: u64,
chunk_size: u32,
total_chunks: u32,
}
download_sync_wave_chunk(
block_hash: Hash,
chunk_index: u32,
) -> {
data: Vec<u8>, // bincode(SyncWaveChunkWire)
chunk_index: u32,
total_chunks: u32,
}Both methods are exposed over the standard wRPC interface, alongside get_account_proof. Clients that already speak to a Sahyadri node for state proofs need no new transport.
The server caches the exported snapshot in memory between chunk requests. The first request for a given block triggers the full export; subsequent chunks for the same block reuse the cache. A request for a different block evicts the cache.
Benchmark Results
SyncWave bootstrap has been observed end-to-end on a local 2-node testnet. The test used real mining, real SMT state, and real cryptographic verification — not a mock.
Test Configuration
- Node A — Mining node, DAG height 258, actively producing blocks at 1 BPS
- Node B — Fresh node, empty data directory, no prior chain state
- Environment — Single laptop, 16 GB RAM, local gRPC (127.0.0.1)
- State size — 5 SMT nodes, 2 account states (~200 bytes payload)
Measured Phases
| Phase | Time |
|---|---|
| Connect to peer (gRPC handshake) | ~4 ms |
| Fetch metadata | ~1 ms |
| Download chunk (5 nodes, 2 states) | ~1 ms |
| SMT verification against checkpoint root | < 1 ms |
| Atomic load into local stores | < 1 ms |
| Total bootstrap time | ~10 ms |
Verification
Node B's rebuilt SMT root equals the checkpoint header's account_commitment at height 258. If any node, any state, or any reference had been corrupted or fabricated, verification would have failed and the bootstrap would have been rejected. The successful bootstrap is therefore cryptographic evidence that the state was transferred intact — not merely that the transfer completed.
Scaling Projection
The test state was minimal (2 accounts). The transfer cost scales with state size; the verification cost stays O(depth) regardless. For production-scale networks:
| Network Scale | Accounts | Est. Payload | Est. Bootstrap |
|---|---|---|---|
| Devnet | 10 | ~1 KB | < 50 ms |
| Testnet | 10,000 | ~1 MB | < 500 ms |
| Production | 1,000,000 | ~100 MB | ~10 sec |
| Large-scale | 10,000,000 | ~1 GB | ~100 sec |
Estimates assume 10 MB/s consumer download (the dominant cost). Local verify and load remain sub-second because they are dominated by disk I/O on content-addressed writes, not by tree traversal. Full IBD for the same networks would take hours to days.
CLI — Fast Bootstrap
Any fresh node can opt into SyncWave bootstrap from a peer via two flags. The peer does not need to be special — every full node serves snapshots.
sahyadrid --sync-wave grpc://seed-node:port --sync-wave-checkpoint 0xabc123... # optional # Or with wrpc (JSON): sahyadrid --sync-wave wrpc://seed-node:port
| Flag | Purpose | Default |
|---|---|---|
--sync-wave <PEER_URL> | Peer to bootstrap from. Format: grpc://host:port or wrpc://host:port. If unset, the node runs ordinary IBD from genesis. | off |
--sync-wave-checkpoint <HASH> | Checkpoint block hash to load. If omitted, the peer's current tip is used. | peer tip |
On startup, the node runs the bootstrap before opening its RPC and P2P ports. A node launched with these flags is fully synced to the checkpoint by the time it appears on the network. If the bootstrap fails — bad peer, unreachable, verification mismatch — the process exits with a clear error rather than starting in a partial state.
--appdir at a fresh path.Security Analysis
A SyncWave snapshot is only as trustworthy as the header it is verified against. Every attack surface is explicit:
| Threat | Mitigation |
|---|---|
| Forged snapshot (fabricated state) | The SMT root is committed in the checkpoint header, which is PoW-protected. A fabricated snapshot would need to hash to the same root — finding such a preimage is beyond any classical or quantum attack on SHA3-256. |
| Truncated snapshot (missing subtrees) | The walk_and_check step rejects any snapshot with a dangling reference. A partial tree cannot have the same root as the full tree. |
| Inflated metadata (DoS via padding) | The declared node count must equal the walk's visit count. An attacker cannot increase the client's workload without failing verification. |
| Stale snapshot (valid root, old block) | The client binds the download to a specific block hash it has verified. A snapshot for an earlier block is simply ignored — the client already knows the tip. |
| Malicious peer mid-download | Chunks are self-contained. A bad chunk is caught at reassembly. The client rotates peers freely; no correctness loss. |
| Replay across forks | Each block has a unique root. A snapshot valid for fork A's checkpoint block is invalid for fork B's. No cross-fork replay possible. |
| Reorg after checkpoint | The checkpoint is chosen deep enough that the selected chain will not reorg past it. If it does, the node falls back to ordinary IBD from an earlier point. |
| Data corruption in transit | Content-addressed nodes detect any corruption: the hash of the corrupt node does not match its key. The client drops the chunk and retries. |
There is no trusted third party in the SyncWave path. The verifier's only assumption is that the checkpoint header was produced by honest consensus — the same assumption every full node makes. SyncWave adds zero new trust assumptions.
Comparison with Full IBD
| Property | Full IBD | SyncWave |
|---|---|---|
| Downloaded data | All blocks + transactions since genesis | One SMT snapshot + headers from checkpoint forward |
| Bootstrap time (1 BPS chain) | Hours to days | Seconds to minutes |
| Verification work | Re-execute every transaction | One hash comparison per node |
| Disk required for full historical replay | Full chain archive | State + headers only |
| Trust in peers | Zero | Zero |
| Archival peers required | Any full node | Any full node with retained state at the checkpoint |
SyncWave does not replace full IBD — it makes full IBD optional for operators who do not need to serve historical blocks. Exchanges, wallets, and validating full nodes that only care about current state can use SyncWave. Block explorers and historical indexers continue to run archival nodes.
Use Cases
Running an Archival Node
Archival nodes retain the full history of the chain and serve SyncWave snapshots for any historical checkpoint. They are the backbone of fast bootstrap — without them, fresh nodes must replay from genesis.
sahyadrid --archival
| Resource | Requirement |
|---|---|
| Storage | Grows with chain (100 GB → TBs over years) |
| RAM | 8 GB minimum, 16 GB recommended |
| Bandwidth | High — serves all peers |
| Permissions | None — anyone can run one |
Any operator can run an archival node. The network benefits from geographic diversity — three or more archival nodes on different continents provide the resilience the network needs.
Limitations
Being honest about what SyncWave does not do:
- It does not replace archival nodes. A SyncWave node does not serve historical blocks it never downloaded. Block explorers and history indexers must still run full IBD.
- It requires a checkpoint with a stable header. The checkpoint block must be deep enough that the selected chain will not reorg past it. The client chooses based on the depth of the current tip.
- It is not private by default. The peer serving the snapshot learns which accounts the client downloads. Privacy requires blinded snapshots or download over a mixnet — outside the current scope.
- It does not compress history. Only state is transferred. Transaction history is not part of a SyncWave snapshot; a node that needs it must run full IBD.
- It assumes the checkpoint header is trusted. A client that does not verify headers is trusting whichever header it was given. Header verification is the trust anchor; the snapshot is the mechanism.
- It requires the serving peer to have retained the state. A pruning node may have discarded old SMT nodes. SyncWave works between archival nodes and fresh nodes, not between two pruning nodes.
None of these are blockers. They are the boundary of what the primitive promises. Everything inside that boundary is cryptographically enforced.
Summary
SyncWave makes fresh-node bootstrap fast without weakening the trust model. A node downloads an untrusted snapshot, verifies it locally against a header it independently trusts, and resumes sync from a checkpoint. The snapshot is not trusted — it is proven. The serving peer is not trusted — it is a pipe. The savings are measured in hours.
The primitive builds on the same machinery that powers state proofs: a single SMT root committed in every block header, content-addressed nodes, and domain-separated hashing. No new cryptography, no new trust assumptions, no new consensus primitive. Just a way to move the state that already exists.
A fresh node, a few hundred sibling hashes, a hash map, one SHA3 walk. That is what stands between an attacker and a forged state bootstrap. It is fast enough to feel like "syncing in seconds" and strong enough to match the guarantees of a node that replayed every block since genesis.