← Complete Projects

阅读中文版本

Build a Merkle Tree Sync

Merkle Tree Sync — a complete walkthrough of merkle tree synchronization, covering leaf hasher, tree builder, root comparer, and testing.

advanced~6 hmerklesynchashdiff

Project goal

Implement merkle tree synchronization that solves detecting which leaves differ between two large sets without transferring every leaf, with each component independently testable.

Final features

  • Leaf hasher
  • Tree builder
  • Root comparer
  • Diff walker
  • Range fetcher

Architecture

The system is organized around leaf hasher, tree builder, root comparer, and diff walker ties them together. Data flows as a binary tree of hashes where each node hashes its children, so a root mismatch localizes the differing subtrees.

Prerequisites

  • Node.js 20 LTS or later
  • Familiarity with merkle tree synchronization fundamentals
  • A local environment to run the test suite

Dependencies

  • blake3 ^0.2.0 — Leaf and node hashing
  • fastify ^5.0.0 — Sync API

Directory structure

merkle_sync_service/
├── src/index.ts
├── src/leaf-hasher.ts
├── src/tree-builder.ts
├── tests/merkle_sync_service.test.ts
└── package.json

Phases

1. Leaf hasher

Build the leaf hasher that anchors merkle tree synchronization and defines the core data shape.

// Leaf hasher: merkle tree synchronization core
export function leafhasher(input: LeafhasherInput): LeafhasherResult {
  // Build the leaf hasher that anchors merkle tree synchronization and defines the core data shape.
  return processLeafhasher(input);
}

Expected behavior: After this phase the leaf hasher works in isolation and is covered by a unit test.

2. Tree builder

Add the tree builder so merkle tree synchronization can detecting which leaves differ correctly.

// Tree builder — handles merkle tree synchronization Add the tree builder so
export class Treebuilder {
  constructor(private opts: TreebuilderOptions) {}
  async run(ctx: Context): Promise<TreebuilderOutput> {
    return this.execute(ctx);
  }
}

Expected behavior: After this phase the tree builder works in isolation and is covered by a unit test.

3. Root comparer

Wire the root comparer to handle a hash collision across without losing data.

// Root comparer: merkle tree synchronization stage
export const rootcomparer = (next: Next) => async (req: Request) => {
  const out = await transform(req);
  return next(out);
};

Expected behavior: After this phase the root comparer works in isolation and is covered by a unit test.

4. Diff walker

Integrate the diff walker to make merkle tree synchronization observable and testable end to end.

// Diff walker for merkle tree synchronization
export async function diffwalker(stream: AsyncIterable<Chunk>): Promise<Aggregate> {
  let acc = init();
  for await (const c of stream) acc = merge(acc, c);
  return acc;
}

Expected behavior: After this phase the diff walker works in isolation and is covered by a unit test.

5. Range fetcher

Finalize the range fetcher so the merkle tree synchronization system degrades gracefully under overload.

// Range fetcher: merkle tree synchronization finalization
export function rangefetcher(state: State): Result {
  validate(state);
  return finalize(state);
}

Expected behavior: After this phase the range fetcher works in isolation and is covered by a unit test.

Testing

Each phase has a focused test: leaf hasher, tree builder, root comparer are exercised in isolation, then an integration test drives the full merkle tree synchronization pipeline and asserts the a hash collision across case is handled.

Troubleshooting

A hash collision across distinct leaves that hides a real difference from the diff walker

Reproduce with a minimal fixture, then add a guard test so the merkle tree synchronization system rejects the condition loudly.

Performance regresses under load

Profile with the sampling analyzer, locate the hot path, and add a targeted benchmark to lock the regression.

Deployment

Run two instances behind a load balancer, share state in Redis, and set resource limits so overload surfaces as backpressure rather than failure.

Known limitations

  • Single-region; multi-region replication is out of scope.
  • The leaf hasher is in-process and not horizontally shared by default.
  • No auth layer; assume deployment behind an edge that enforces identity.

Project overview

This project builds a merkle tree synchronization system whose reason for existing is detecting which leaves differ between two large sets without transferring every leaf. The temptation in merkle tree synchronization is to bolt the interesting part onto a working prototype and discover the hard cases in production; this build inverts that order and treats a hash collision across distinct leaves that hides a real difference from the diff walker as the first thing designed, not the last.

You will implement Leaf hasher, Tree builder, Root comparer, Diff walker, and Range fetcher, each as a module with one responsibility and a test that proves it in isolation. The merkle tree synchronization pipeline is then the wiring that composes them, and the integration test is where the composition is shown to survive a hash collision across distinct leaves that hides a real difference from the diff walker.

The walkthrough is written for a developer who has used a merkle tree synchronization system but has not had to make one honest. It favors explicit data shapes over clever abstractions, and it names the failure each design choice prevents rather than presenting the choice as a style preference.

Each phase ends with something observable — a leaf hasher that returns the right shape, a tree builder that rejects bad input, an integration test that goes green — so you can stop at any point and still have a merkle tree synchronization system that works for the cases covered so far.

What you will build

The finished merkle tree synchronization system exposes five capabilities, each with a focused test:

  • Leaf hasher
  • Tree builder
  • Root comparer
  • Diff walker
  • Range fetcher

Leaf hasher is the foundation that every later stage assumes; Tree builder is where merkle tree synchronization correctness lives; Root comparer handles a hash collision across distinct leaves that hides a real difference from the diff walker; Diff walker makes the system observable; and Range fetcher is the graceful-degradation layer that keeps overload from becoming corruption.

The build keeps the merkle tree synchronization logic in ordinary modules you can read in one sitting, and pulls in dependencies only for the parts that are genuinely tedious to hand-roll. That keeps the diff between the tutorial and a real codebase small, and keeps the failure modes visible in code rather than hidden behind a framework.

Architecture and data model

The system is organized around Leaf hasher, Tree builder, and Root comparer, with Diff walker tying them together. Data flows as a binary tree of hashes where each node hashes its children, so a root mismatch localizes the differing subtrees.

Each component is a module with a pure interface where practical: the same input produces the same output, and side effects are pushed to the edges. That discipline is what makes the phases independently testable, because a test for Root comparer does not need to stand up Leaf hasher and Tree builder to exercise the interesting branch.

The data model is deliberately explicit. A binary tree of hashes where each node hashes its children, so a root mismatch localizes the differing subtrees Keeping the shape written down — in a schema, a type, or a fixture — means a change to the shape is a visible diff rather than a silent runtime shift, and it means the integration test can assert against a concrete shape instead of guessing.

Configuration and code are kept apart: the configuration says what merkle tree synchronization should do, the modules say how. A reviewer can change a threshold or a policy without reading the implementation, which keeps the risky changes small and lets the diff stand as the review.

The failure path through Root comparer is designed before the happy path is finished, because a hash collision across distinct leaves that hides a real difference from the diff walker is the case that distinguishes a merkle tree synchronization system from a demo. Every component declares what it does when its inputs are wrong, and the integration test asserts each of those declarations.

Prerequisites and dependencies

Have these ready before you install anything, so the merkle tree synchronization setup is not blocked by a missing tool:

  • Node.js 20 LTS or later
  • Familiarity with merkle tree synchronization fundamentals
  • A local environment to run the test suite

The runtime dependencies are chosen to keep the merkle tree synchronization datapath legible:

  • blake3 (^0.2.0) — Leaf and node hashing
  • fastify (^5.0.0) — Sync API
# setup for merkle-sync-service
pnpm init
pnpm add blake3 fastify
pnpm add -D typescript tsx vitest

Versions are pinned to a major range so the merkle tree synchronization install is reproducible; commit the lockfile. The leaf hasher reads its operational parameters from the environment and validates them at startup, so a missing value fails the boot rather than the first merkle tree synchronization request.

Directory structure

Keep each merkle tree synchronization component in its own file so it can be tested without starting the others:

merkle_sync_service/
├── src/index.ts
├── src/leaf-hasher.ts
├── src/tree-builder.ts
├── tests/merkle_sync_service.test.ts
└── package.json

Phase 1: Leaf hasher

Build the leaf hasher that anchors merkle tree synchronization and defines the core data shape.

// Leaf hasher: merkle tree synchronization core
export function leafhasher(input: LeafhasherInput): LeafhasherResult {
  // Build the leaf hasher that anchors merkle tree synchronization and defines the core data shape.
  return processLeafhasher(input);
}

After this phase the leaf hasher works in isolation and is covered by a unit test. The leaf hasher defines the data shape that every later stage assumes, so getting it right first means the later phases compile against a stable contract.

Treat the leaf hasher as the single source of truth for the merkle tree synchronization data shape. Validate inputs at the boundary and reject early, so a malformed value never reaches Tree builder or Root comparer where its effect would be harder to localize.

Phase 2: Tree builder

Add the tree builder so merkle tree synchronization can detecting which leaves differ correctly.

// Tree builder — handles merkle tree synchronization Add the tree builder so
export class Treebuilder {
  constructor(private opts: TreebuilderOptions) {}
  async run(ctx: Context): Promise<TreebuilderOutput> {
    return this.execute(ctx);
  }
}

After this phase the tree builder works in isolation and is covered by a unit test. This is where merkle tree synchronization correctness lives: the tree builder is what makes detecting which leaves differ between two large sets without transferring every leaf true, and its test is the proof.

Keep the tree builder pure where you can. A function that takes an input and returns an output is trivial to test and impossible to mock wrong; a class with hidden state is the opposite. Push side effects to the caller and keep the tree builder about the transformation.

Phase 3: Root comparer

Wire the root comparer to handle a hash collision across without losing data.

// Root comparer: merkle tree synchronization stage
export const rootcomparer = (next: Next) => async (req: Request) => {
  const out = await transform(req);
  return next(out);
};

After this phase the root comparer works in isolation and is covered by a unit test. The root comparer is the difference between a merkle tree synchronization system that works on the happy path and one that survives a hash collision across distinct leaves that hides a real difference from the diff walker.

Design the failure before the success. Decide what root comparer does when a hash collision across distinct leaves that hides a real difference from the diff walker: reject, retry, shed, or degrade — and write that test first. A merkle tree synchronization system whose failure behavior is unspecified will pick one at random under pressure, and it is usually the wrong one.

Phase 4: Diff walker

Integrate the diff walker to make merkle tree synchronization observable and testable end to end.

// Diff walker for merkle tree synchronization
export async function diffwalker(stream: AsyncIterable<Chunk>): Promise<Aggregate> {
  let acc = init();
  for await (const c of stream) acc = merge(acc, c);
  return acc;
}

After this phase the diff walker works in isolation and is covered by a unit test. Observability is not a finishing touch; it is how you know the merkle tree synchronization system is doing what it claims in production.

Emit one structured event per meaningful action, with enough context to localize a failure without a second round-trip. The diff walker is what turns “something is slow” into “the tree builder is slow for inputs matching this shape,” which is the difference between an alert and an investigation.

Phase 5: Range fetcher

Finalize the range fetcher so the merkle tree synchronization system degrades gracefully under overload.

// Range fetcher: merkle tree synchronization finalization
export function rangefetcher(state: State): Result {
  validate(state);
  return finalize(state);
}

After this phase the range fetcher works in isolation and is covered by a unit test. The range fetcher is what keeps overload from becoming corruption: when the merkle tree synchronization system is asked for more than it can do, it degrades in a defined way instead of failing in an undefined one.

Pick a degradation strategy for range fetcher — shed, queue, or backpressure — and make it explicit in code and in a test. Leaving the behavior to chance is how a merkle tree synchronization traffic spike becomes a data loss incident.

Testing

Each phase has a focused test: leaf hasher, tree builder, and root comparer are exercised in isolation, then an integration test drives the full merkle tree synchronization pipeline and asserts the a hash collision across case is handled.

Start with unit tests that pin each component’s contract: the leaf hasher returns the documented shape, the tree builder transforms correctly, and the root comparer fails in the documented way. These tests are fast and they localize regressions to the component that broke.

Then add an integration test that composes the components and drives a realistic merkle tree synchronization scenario end to end. Assert the happy path first, then deliberately trigger a hash collision across distinct leaves that hides a real difference from the diff walker and assert the system degrades rather than corrupts. Because the components are pure where practical, the integration test stays deterministic rather than flaky.

A merkle tree synchronization suite must also prove the negative: bad input is rejected at the boundary, overload is shed rather than queued indefinitely, and a partial failure does not leave the system in an inconsistent state. These are the tests that catch the regressions a happy-path suite never would.

The entire merkle tree synchronization suite runs with no external service; every dependency is either in-process or stubbed, so every case is reproducible in CI. That is what lets the root comparer failure-mode assertions live as tests rather than as a manual checklist that drifts out of date.

# run the merkle tree synchronization test suite
pnpm vitest run

Deployment and operations

Run at least two merkle tree synchronization instances behind a load balancer and share state in Redis. Set resource limits so merkle tree synchronization overload surfaces as backpressure rather than failure.

Run at least two stateless instances behind a load balancer, and move any shared state into a shared store so the merkle tree synchronization behavior is global rather than per instance. Size limits so the system sheds load before it runs out of memory, because a merkle tree synchronization system that OOMs under load fails in a way that is hard to reason about.

Drain on deploy: stop accepting new work, let in-flight merkle tree synchronization operations finish, then exit. Because the system holds no state that cannot be reconstructed, a rollout is a matter of cycling instances one at a time while watching the error rate.

When a dependency is unavailable, decide explicitly whether the merkle tree synchronization system fails closed or degrades. Leaving that choice to chance is how a dependency outage becomes a silent data loss, so make the choice visible in code and in a test.

Security considerations

Validate all merkle tree synchronization inputs at the boundary, reject oversized payloads before buffering them, and keep secrets in the environment rather than the repository.

The merkle tree synchronization system is a choke point, so it is the right place to reject malformed input before it reaches tree builder or root comparer. Validate the shape and the size at the edge, and let the inner components assume what they receive is well-formed.

Never log secrets or full payloads; log enough to localize a failure without recording data that should not be on disk. Treat any external call as untrusted and validate its response before using it, because a merkle tree synchronization system that trusts its inputs blindly is one malformed response away from corruption.

Keep dependencies pinned and run an audit in CI. A transitive dependency with a known vulnerability is still a vulnerability in your merkle tree synchronization system, and the lockfile is what makes the audit meaningful.

Observability

Emit one structured event per merkle tree synchronization operation with the inputs’ shape, the outcome, and a correlation id. Track error rate and latency per component and alert on a step change rather than a single spike.

The correlation id is the thread that ties a user-visible symptom to the merkle tree synchronization decision and to the component log entry for the same operation. Without it, an investigation starts from a timestamp and a guess.

Watch the root comparer failure rate separately from the overall error rate. A jump in root comparer failures usually means a hash collision across distinct leaves that hides a real difference from the diff walker, not a bug in tree builder, and the two have different owners and different fixes.

Chart the share of merkle tree synchronization operations that shed, queue, or succeed. A change in the mix points at the cause faster than an aggregate latency can, because each class has a different root.

Troubleshooting

A hash collision across distinct leaves that hides a real difference from the diff walker. Reproduce with a minimal fixture, then add a guard test so the merkle tree synchronization system rejects the condition loudly.

Performance regresses under merkle tree synchronization load. Profile with the sampling analyzer, locate the hot path in tree builder, and add a targeted benchmark to lock the regression.

Keep the merkle tree synchronization dashboards separate from dependency dashboards. When an operation fails, the correlation id lets you decide in seconds whether the fault is leaf hasher, tree builder, or root comparer, instead of guessing from a timeout.

Limitations and next steps

  • Single-region; multi-region replication is out of scope.
  • The leaf hasher is in-process and not horizontally shared by default.
  • No auth layer; assume deployment behind an edge that enforces identity.

The merkle tree synchronization system is single-region by design; extending to multi-region means making leaf hasher shareable across regions, which is a real change to the consistency model rather than a config flag. The root comparer is in-process, which is correct for one instance but must move to a shared store before scaling out.

There is no authentication or authorization layer; the system assumes it is deployed behind an edge that enforces identity. Adding one is straightforward but out of scope for this build, and doing it here would distract from the merkle tree synchronization correctness that is the point.

The natural next step, once the merkle tree synchronization pipeline is trusted in production, is to add a short-TTL cache for idempotent reads and a richer diff walker that exports histograms rather than counters. Both are additive and neither changes the contracts this build establishes.