Why We Run Zakura on Different CPUs and Compilers


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.
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.