Why We Run Zakura on Different CPUs and Compilers

Two ink chips, one labelled LLVM x86-64 and one labelled Cranelift ARM64, each send arrows toward a pink disc in the middle holding a large question-mark-over-equals sign. Drawn in charcoal and pink on cream paper.Two outlined chips, one labelled LLVM x86-64 and one labelled Cranelift ARM64, each send arrows toward a plum disc in the middle holding a large question-mark-over-equals sign. Drawn in pale ink and pink on a night background.

Compilers can make correct code produce the wrong result. Zakura nodes running the same build share any miscompilation, so their agreement may not surface a low-level platform-specific bug baked into every binary.

To make this kind of failure observable, we introduced a node whose build shares as little as possible with regular ones. Regular nodes run Linux on x86-64, compiled with LLVM, and the new node runs on an ARM64 Mac, compiled with Cranelift. Monitoring compares what these nodes compute, so a failure that identical builds would hide becomes visible within minutes.

Different Paths Through the Same Code

Building a Rust program has two stages. The first checks the source code and translates it into a simplified intermediate form. The second stage, the backend, turns that form into machine instructions for a particular CPU. Our usual builds use LLVM for that second stage. The Mac node relies on the independently written Cranelift backend.

One Zakura source tree enters a shared rustc front end, which checks the code and lowers it to an intermediate form. From there the paths split: the LLVM backend emits x86-64 machine code for the Linux fleet with its AVX2 and AVX-512 paths, while the Cranelift backend emits ARM64 machine code for one Mac node with its NEON paths. Monitoring compares what the two builds compute and makes any disagreement visible.Zakura source coderustc front endcompiles to an intermediate formLLVM backendx86-64 machine codeLinux fleet · AVX2 / AVX-512 pathsCranelift backendARM64 machine codeone Mac node · NEON pathsMonitoringcompares results, makes disagreement visible
The two build paths. Everything after the shared front end differs: backend, instruction set, and the CPU-specific routines each binary selects.

Some libraries contain separate implementations for different processors, selected at compile time or through runtime feature detection. Changing architectures can therefore change both the generated instructions and the library routines that execute.

For example, a hashing or large-integer arithmetic library might select an AVX2 or AVX-512 routine on x86-64 and a NEON routine on ARM64; a defect in the x86-only variant never runs on the Mac. Bugs in LLVM's translation, or in the x86-specific code paths, will not repeat there. Both nodes run the same Zakura source code, so, when they disagree, those two layers are the first place to look.

Some parts remain shared. Both builds go through the same first translation stage, so a logic bug there affects both backends. Cranelift also compiles only Rust: the C libraries Zakura depends on, and the operating system's own libraries, are built with their usual compilers on each platform.

Architecture-specific Bugs in the Wild

In April 2026, OpenSSL disclosed an out-of-bounds read (CVE-2026-28386) in its AVX-512/VAES implementation of AES-CFB128. Other architectures used unaffected implementations. OpenSSL rated the issue low severity, but it illustrates why architecture diversity matters: the ARM build never executes the faulty routine.

The Failure We Worry About

When a node validates a shielded transaction, it cannot inspect the notes being spent. It relies on zero-knowledge proofs, signatures, and other consensus checks to establish that the spends are valid and authorized, and that the transaction conserves value.

Now suppose a miscompiled verifier on the x86 build accepts a proof claiming to spend a note that never existed. Real wallets never generate such a proof, but an attacker who knows about the flaw can go looking for one. Once found, the proof lets them mint value that was never shielded in. Every node on that build accepts the transaction, and, since all nodes agree, nothing looks wrong.

Zcash's turnstile (ZIP 209) limits what happens next: withdrawals from a shielded pool can never exceed what was shielded into it. That caps the damage at the pool's balance, but it also turns the counterfeit into a claim on real users' funds. Whoever withdraws first is paid, and the pool is left short. This limits the loss, but for some bugs we would much rather detect them immediately than when the pool is drained.

The ARM64 node compiled with Cranelift would reject the block containing that transaction and stop advancing. If a compiler instead drops a carry (the value passed between machine words when a large-integer calculation overflows one word) while hashing a commitment tree, no block is rejected: both nodes keep advancing but compute different tree roots.

Turning Disagreement Into a Response

The monitor continuously compares committed results at corresponding block heights, a few blocks behind the shortest chain tip across all nodes. It checks block hashes and the roots and frontiers of the Sapling, Orchard, and Ironwood commitment trees. These trees cryptographically summarize the shielded outputs each node has accepted, so comparing them validates what each node computed.

The above failures become visible in different ways. The dropped carry produces a tree mismatch at a height that both nodes have reached, and the monitor flags it immediately. The proof that only one build rejects never gets a matching height to compare: the Cranelift node stops while the LLVM node moves on, and the monitor flags the stall after a few minutes.

This check requires a bug to produce different observable results on each compiler backend. It cannot catch code or protocol design bugs that run the same way on both builds. For example, Bitcoin's 2018 inflation bug (CVE-2018-17144) was of that kind: a flaw in Bitcoin Core's source would have let a miner spend the same coin twice within one transaction and create new bitcoin. Every build carried it, so no build disagreed.

The Broader Pattern

Running across architectures and backends adds one more layer of security to catch problems sooner, alongside the testing and review that already support Zakura's best-in-class validation suite. It is also one instance of a broader pattern: a failure every node shares is a failure no node can witness, so the fewer layers the nodes have in common, the more kinds of bugs turn into visible disagreement. The build stack is where we started. The same principle extends to the layers all nodes still share, independent implementations of the verification logic among them.