DEV Community

zk_nd3r
zk_nd3r

Posted on

Verifying Zcash Proofs on Ethereum with EIP-152

Ethereum has a precompile that almost nobody knows about. It lives at address 0x09, it computes the BLAKE2b compression function, and it was put there specifically so you can verify Zcash on Ethereum. This is the story of how we use it.

What EIP-152 Is

EIP-152 landed in the Istanbul hard fork (December 2019). It exposes the BLAKE2b F compression function as a precompiled contract at address 0x09. Cost: 1 gas per round. A standard BLAKE2b call runs 12 rounds, so 12 gas total.

BLAKE2b is the hash function underpinning Zcash's Sapling and NU5 Merkle trees. Without this precompile, computing BLAKE2b in Solidity costs around 200,000 gas. With it: 712 gas for a full hash. That is a 280x reduction.

The EIP was proposed by Tjaden Hess and others from the Ethereum Foundation and was motivated by one thing: enabling Zcash light client verification on Ethereum without absurd gas costs.

Why It Matters

Cross chain proof verification needs hash functions. Zcash uses BLAKE2b with personalization strings for domain separation. Each tree level, each protocol context uses a different personalization. For example, ZcashPedersenHash for note commitment trees and ZTxIdHeadersHash for transaction IDs.

If you cannot compute these hashes cheaply on chain, you cannot verify Zcash state on Ethereum. Period. The precompile makes it possible.

The 213 Byte Input Format

The precompile expects exactly 213 bytes, packed tight:

  • rounds (4 bytes): number of rounds, 12 for BLAKE2b
  • h (64 bytes): state vector, 8 x uint64 little endian
  • m (128 bytes): message block
  • t[0] (8 bytes): offset counter low, uint64 little endian
  • t[1] (8 bytes): offset counter high, uint64 little endian
  • f (1 byte): final block flag, 0 or 1

Total: 213 bytes in, 64 bytes out.

The Precompile Call

Here is how ZAP1Verifier.sol calls it:

function blake2b(
    uint32 rounds,
    bytes memory h,
    bytes memory m,
    uint64 t0,
    uint64 t1,
    bool isFinal
) internal view returns (bytes memory) {
    bytes memory input = abi.encodePacked(
        bytes4(rounds),
        h,
        m,
        bytes8(t0),
        bytes8(t1),
        isFinal ? bytes1(0x01) : bytes1(0x00)
    );

    (bool ok, bytes memory out) = address(0x09).staticcall(input);
    require(ok, "BLAKE2b precompile failed");
    return out;
}
Enter fullscreen mode Exit fullscreen mode

staticcall to 0x09. No ABI encoding. No function selector. Raw bytes in, raw bytes out. The precompile handles the rest.

Personalization Strings

Zcash domain separation works by XOR ing a 16 byte personalization string into the initial state vector (bytes 32 to 47 of h). Each protocol context gets its own string. When verifying a Sapling note commitment Merkle path, you set the personalization to the appropriate Zcash constant before each compression call.

This is not optional. Wrong personalization means wrong hash means failed verification. The precompile does not enforce personalization; your contract must set h correctly before calling 0x09.

Gas Reality

A Merkle proof for a Zcash Sapling tree is 32 levels deep. Each level needs one BLAKE2b compression. With the precompile, that is roughly 32 x 712 = ~22,800 gas for the hashing. In pure Solidity: 32 x 200,000 = 6.4M gas. One would blow past block gas limits.

The precompile does not just save money. It makes the verification possible at all.

Live on Mainnet

ZAP1Verifier is deployed on ETH mainnet at 0x12db453A7181E369cc5C64A332e3808e807057C1. It verifies Zcash Sapling Merkle proofs using the EIP-152 precompile. The same contract is live on Arbitrum, Base, Hyperliquid, and Sepolia.

Source: github.com/Frontier-Compute/zap1-verify-sol

The Bottom Line

The precompile has been on Ethereum since 2019. Almost nobody uses it. We do.

Top comments (0)