Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Delta Compression and Merkle Trees for State Synchronization

Recommended resources

Hello! Welcome back.

In our last lesson, we established event sourcing as the foundational pattern for building an auditable, reconstructible history of all operations in your supply chain system. The result is an append-only log of events for each entity—a container, a vehicle, a piece of cargo. While this provides a perfect audit trail, it also presents a significant challenge for your 20,000-node mesh network: how do you keep all these distributed logs in sync without overwhelming the limited bandwidth of your Teltonika FMB920 devices?

This brings us to today's lesson, which directly addresses the learning outcome: Implement efficient state synchronization using delta compression and Merkle trees.

We will explore how to move beyond naively sending entire state objects and instead transmit only the minimal changes, or "deltas." You'll learn how Merkle trees provide a powerful and efficient mechanism to discover these deltas between two nodes with minimal data exchange, a technique essential for building scalable, offline-first distributed systems.


1. The Core Idea: Synchronizing with Deltas

When a device comes back online, it needs to synchronize its local state with the rest of the network. Sending the entire event log or even a full snapshot of the current state is highly inefficient. The core principle of efficient synchronization is delta compression: computing the difference between two versions of a file or data object and sending only that difference.

Delta Compression Engine Concepts
This diagram illustrates the fundamental concept of delta compression. Instead of transmitting the entire new file, a delta engine computes a smaller 'update file' containing only the changes. The receiver can then apply this update to its old file to reconstruct the new version.

The central problem, however, is that for one node to compute a delta, it must know exactly what version of the data the other node possesses. In a distributed system with thousands of intermittently connected nodes, this is not a trivial challenge. How can two nodes efficiently agree on their differences without first sending all their data to each other? This is where Merkle trees come in.



2. Merkle Trees: A "Fingerprint" for Your Data

A Merkle tree, or hash tree, is a data structure that allows for the efficient and secure verification of the contents of large data structures. In essence, it provides a single, small cryptographic hash that acts as a "fingerprint" for an entire dataset. If even one byte of the data changes, the final hash will change completely.

Merkle Tree Construction
This diagram shows how a Merkle tree is built. The data is first broken into chunks (A, B, C, D, E). Each chunk is hashed to create a leaf node (H1-H5). Pairs of leaf hashes are then concatenated and hashed to create parent nodes (H6, H7), and this process continues up the tree until a single root hash (R) is produced.

By comparing root hashes, two nodes can instantly determine if their datasets are identical. If the hashes differ, they can traverse the tree to pinpoint the exact data chunks that are different, all without transmitting the data itself.

To get a solid grasp of this process, the following video provides a clear and concise explanation.

Google SWE teaches systems design | EP42: Merkle Trees

Watch this short video from the "Google SWE teaches systems design" series. It explains the core algorithm of Merkle trees and their application in anti-entropy processes, which is exactly how we'll use them for synchronization.

Please watch from 00:56 to 02:27. Focus on two key aspects: how the tree is constructed from leaf hashes to a root hash, and how a change in a single file propagates up to change the root, allowing for efficient discovery of the changed data.

The Synchronization Protocol

As the video demonstrates, the protocol for synchronizing two nodes (let's call them A and B) is a recursive process:

  1. Compare Roots: Node A sends its root hash to Node B. If the hashes match, they are in sync. End of process.
  2. Compare Children: If the root hashes differ, Node A requests the hashes of Node B's children. Node A compares these hashes to its own child hashes.
  3. Descend Recursively: For any child node where the hashes differ, A and B repeat the process, descending deeper into that branch of the tree.
  4. Transfer Data: This continues until they reach a leaf node with differing hashes. At this point, Node A knows this specific data chunk is out of sync and requests the correct version from Node B.

This allows the nodes to identify all differing data chunks by only exchanging small hashes, dramatically reducing network bandwidth compared to sending the entire dataset. The only data that needs to be fully transmitted are the chunks that are actually different.

Test your understanding!

Imagine two nodes have a dataset consisting of 16 data blocks. The Merkle tree is a balanced binary tree. If only block #12 is different, how many rounds of hash exchanges are needed to pinpoint this single differing block? (A "round" consists of one node sending its child hashes and the other node comparing them).

Show answer

The tree would have a depth of 4 (since ). The process would be:

  1. Round 1 (Root): Root hashes differ.
  2. Round 2 (Level 1): The hash of the right child (covering blocks 9-16) will differ.
  3. Round 3 (Level 2): The hash of the left child of that node (covering blocks 9-12) will differ.
  4. Round 4 (Level 3): The hash of the right child of that node (covering blocks 11-12) will differ.

At this point, the nodes are at the leaf level's parent. They compare the hashes for block #11 (match) and block #12 (mismatch). It takes 4 rounds of hash exchanges to traverse the tree from the root to the differing leaf.



3. Advanced Implementations for Robustness and Efficiency

A basic Merkle tree implementation is a great start, but for your demanding use case—synchronizing an auditable event log across a massive, unreliable mesh network—we can incorporate more sophisticated techniques that you, as a systems architect, should evaluate.

3.1. Robust Chunking: Content-Defined Chunking (CDC)

How you split your data into the initial "chunks" (the leaves of the tree) is critical. A simple approach is Fixed-Sized Chunking (FSC), where you split the data every 4KB, for example. But this has a major flaw. If you insert a single byte at the beginning of your data, every subsequent chunk is shifted, causing all their hashes to change. The Merkle tree would then indicate that the entire file is different, defeating the purpose.

A more robust solution is Content-Defined Chunking (CDC). Instead of using fixed offsets, CDC uses a rolling hash algorithm to find "natural" boundaries in the data itself.

NetSync: A Network Adaptive and Deduplication-Inspired ...

The academic paper "NetSync" provides an excellent motivation for using CDC over traditional methods like those in rsync. We'll focus on the section that explains this core problem and introduces CDC as the solution.

Please read the section titled "3 CHALLENGES AND MOTIVATIONS" (starts on page 4). Pay close attention to the paragraphs on "The Weak Point of Fix-Sized Chunking" and "Introducing Content-Defined Chunking (CDC)". This will clarify why CDC is essential for handling real-world data modifications.

By making chunk boundaries dependent on content, CDC ensures that an insertion or deletion only affects the chunks immediately around it. The rest of the chunks remain unchanged, allowing the Merkle tree to correctly identify that only a small portion of the data has actually been modified. For synchronizing event logs where new events are constantly being added, this is a far more resilient approach.

3.2. Optimizing for Ordered Data: Merkle Search Trees (MST)

The event logs we designed in the last lesson have a natural order: time. Standard Merkle trees are built on unordered sets of data. However, we can use a specialized structure that leverages this ordering for even greater efficiency.

A Merkle Search Tree (MST) combines a balanced search tree (like a B-tree) with a Merkle tree. It preserves the key order of the data. When you add a new range of items (like a batch of new events at the end of your log), they form a compact new subtree. This locality means that synchronizing recent changes often requires traversing only a very small, specific part of the tree.

Merkle Search Trees: Efficient State-Based CRDTs in Open ...

The paper "Merkle Search Trees: Efficient State-Based CRDTs in Open Networks" introduces this powerful data structure. Its combination of key-ordering and hashing is perfectly suited for synchronizing the CRDT-based event logs we're designing.

Please read Section III. THE MERKLE SEARCH TREE DATA STRUCTURE (page 3). Focus on understanding the three key features it combines: deterministic representation, key order preservation, and balance. Also, read subsection D. Efficiency for Dataset Comparison.

For your use case, where new events are always appended chronologically, an MST would be exceptionally efficient. When two nodes compare their trees, the differences will almost always be concentrated at the "end" of the tree structure, leading to very few hash comparisons needed to find and transmit the new events.

3.3. Adapting to the Network

Finally, a truly robust system should adapt its strategy to the environment. The trade-offs between computational cost (CPU) and network cost (bandwidth) are not static.

  • On a high-bandwidth, low-latency link (e.g., two servers in a data center), you might prioritize speed. You could use larger chunk sizes and lighter compression to minimize CPU usage, as network transfer is cheap.
  • On your low-bandwidth, high-latency FMB920 mesh network, the priority is to minimize every byte sent. Here, you would favor smaller chunk sizes (to find more granular similarities) and stronger compression, even if it requires more processing power on the device.

The "NetSync" paper (LINK) proposes exactly this: a network-adaptive parameter selector that dynamically chooses chunking granularity and compression methods based on monitored network conditions. This is an advanced architectural consideration that directly aligns with your goal of building a resilient system for the unpredictable conditions at the port.



Conclusion

Today, we've designed the engine for efficient state synchronization in your large-scale distributed system. We moved from the high-level concept of delta sync to the concrete implementation details required for a robust, production-grade architecture.

Key Takeaways:

  • Merkle trees are the core mechanism that enables two nodes to efficiently find differences in large datasets by exchanging only a small number of hashes.
  • The synchronization protocol involves a recursive descent down the tree, only exploring branches where hashes do not match.
  • Content-Defined Chunking (CDC) is a superior method for splitting data into leaves, as it's resilient to the "boundary-shift" problem caused by insertions and deletions.
  • For ordered data like your event logs, a Merkle Search Tree (MST) is a highly optimized structure that leverages data locality to speed up synchronization.
  • A production system should be network-adaptive, adjusting parameters like chunk size and compression to balance CPU usage and bandwidth consumption based on current network conditions.

In our next lesson, we will build directly on this foundation. Now that we know how to efficiently represent the differences between two nodes, we need a protocol for these differences to spread across the entire 20,000-node network. We will address the learning outcome: Design a gossip protocol for disseminating state updates epidemically across the 20,000-node mesh.

End of Module,Exit lesson