Home · ASPLOS 2023 · Security, Privacy & Trusted Execution

GZKP: A GPU Accelerated Zero-Knowledge Proof System

Weiliang Ma, Qian Xiong, Xuanhua Shi, Xiaosong Ma, Hai Jin, Haozhao Kuang and 4 others

Conference
ASPLOS 2023
Published
2023
Pages
340-353
Citations
44

A zero-knowledge proof verifies in milliseconds but takes half a minute to GENERATE, because proving means millions of operations on 256–753-bit integers — GZKP rebuilds both prover stages for the GPU (an NTT that never shuffles global memory, an MSM that merges every window into one bucket set, and a big-integer library running in double-precision floats) for an order-of-magnitude speedup over the best prior GPU provers.

The argument in one glance

  1. Problem

    Proving takes 30 seconds; verifying takes 10 milliseconds

    A zero-knowledge proof lets you convince anyone a computation was done correctly without revealing its private inputs. The zkSNARK flavor makes the RECEIVING end nearly free — proofs under a kilobyte, verified in under 10 ms — but the SENDING end pays for that luxury. Generating one proof for a private Zcash transaction takes more than 30 seconds on a CPU, because it grinds through millions of arithmetic operations on integers 256 to 753 bits wide, where a single modular multiplication costs around 230 nanoseconds.

  2. What people did

    Put the two hot kernels on a GPU

    Proof generation boils down to two stages: a POLY stage that runs a series of number-theoretic transforms (NTTs — FFTs over a finite field), and an MSM stage that computes huge inner products of scalars and elliptic-curve points, typically over 70% of CPU proving time. Both look embarrassingly parallel, so industry systems appeared: MINA moved MSM to the GPU, and Filecoin's bellperson moved both stages.

  3. Why it fell short

    753-bit numbers break the textbook parallelizations

    GPU FFT tradition shuffles data between iterations so each thread reads contiguous memory — cheap for 8-byte doubles, but with up to 96-byte field elements the shuffling itself consumes 42–81% of each NTT batch's time. MSM's standard Pippenger algorithm ends in a window-reduction step that is inherently serial, its natural preprocessing wants over 5 GB of GPU memory, and real workloads' sparse scalars leave some buckets 2.85× heavier than average, idling most warps. Net effect: MINA barely beat a well-tuned CPU end to end.

  4. This paper's idea

    Redesign both kernels around what huge integers do to a GPU

    GZKP never shuffles global memory in the NTT (small independent groups per thread block, reordered internally through shared memory, big integers stored column-major), and merges every MSM window into ONE bucket set using precomputed weighted points — eliminating the serial reduction and enabling fine-grained load balancing — all on a finite-field library that packs 52-bit limbs into double-precision floats. End to end: on average 48.1× faster than the best CPU prover and 33.6× faster than the best GPU prover on 753-bit workloads.

Why this problem is worth solving

A

a cryptographic protocol in which a prover convinces a verifier that a statement is true — “this transaction is valid,” “this computation was done correctly” — without revealing any of the private data behind it
has an unusual economic shape. In the
Succinct Non-interactive ARgument of Knowledge — the practical ZKP family used by Zcash, Filecoin, and most blockchain privacy systems; “succinct” means the proof is tiny and fast to check regardless of how big the proven computation was
family, the proof is smaller than a kilobyte and anyone can check it in under 10 milliseconds. That is exactly what a blockchain needs: thousands of nodes verify, so verification must be nearly free.

But the cost didn’t vanish — it moved to the prover. Every private Zcash transaction requires its sender to generate a proof, and that takes over 30 seconds of CPU time. Filecoin’s storage proofs, private smart contracts, verifiable databases, even proofs about machine-learning predictions all sit behind the same bottleneck. The arithmetic is brutal by construction: zkSNARK security rests on

a number system with a fixed prime modulus where addition and multiplication wrap around — all zkSNARK arithmetic happens on integers modulo a prime that is 256 to 753 bits long, far beyond what any CPU or GPU register holds natively
elements so large that one modular multiplication takes ~230 ns and one addition 43 ns on a CPU. Multiply by the millions of operations in a single proof and the seconds add up. If proving could drop to tens of milliseconds, zero-knowledge proofs stop being a batch operation and start being something an online service can do per request.

What people did before, and where it broke

Proof generation has exactly two expensive stages. The POLY stage runs a series of

number-theoretic transform — the FFT’s finite-field twin. Same butterfly structure and O(N log N) cost as an FFT, but every element is a huge modular integer instead of a complex number
operations (seven per proof in libsnark’s zkSNARK) to build a polynomial the protocol needs. The MSM stage computes
multi-scalar multiplication — the inner product Σ sᵢ⊗Pᵢ of a vector of scalars with a vector of elliptic-curve points, where ⊗ means adding a point to itself sᵢ times. Five of these per proof, and over 70% of CPU proving time
over vectors that can reach millions of entries.

Both kernels have decades of parallel-computing literature behind them, and the existing GPU provers imported it directly. That is where things broke:

  • NTT: the shuffle that ate the kernel. A GPU wants the 32 threads of a
    the GPU’s scheduling unit — 32 threads that execute in lockstep and achieve full memory bandwidth only when they access consecutive addresses (a “coalesced” access)
    to read consecutive addresses, but the NTT’s butterfly pattern reads elements a large stride apart. The classical fix — physically shuffle the array between iteration batches so the next batch reads contiguously — is nearly free for 8-byte floats. With 381-bit or 753-bit elements (48–96 bytes each), the shuffle is itself a giant data movement: measured in bellperson, shuffle steps consume 42–81% of each batch’s time. Skipping the shuffle is no better — the strided accesses it prevents grow from 13% to 53% of runtime between consecutive batches at scale 2²⁴.
  • MSM: a serial tail and unbalanced buckets. Every fast MSM uses
    the classic trick for computing Σ sᵢ⊗Pᵢ — slice each scalar into windows of k bits, and within each window throw points into 2ᵏ−1 “buckets” by their k-bit digit. Sum each bucket, weight buckets by their digit (bucket-reduction), then combine window results with repeated doubling (window-reduction). It replaces per-scalar doubling chains with mostly plain point additions
    . On a GPU the point-merging phase parallelizes well, but the final window-reduction is a dependency chain that must run serially — bellperson ships it back to the CPU. Worse, real proof workloads have sparse scalar vectors (mostly zeros and ones, an artifact of how circuits encode bound checks), so bucket sizes vary wildly: the heaviest bucket holds 2.85× the average, and every warp waits for the slowest one. This is why MINA’s GPU barely improved on libsnark’s CPU end to end.
  • Big-integer arithmetic itself. GPUs provision far more floating-point than integer multiply throughput, yet every prior prover ran its field arithmetic on the integer units, with each 753-bit number spread across 24 thirty-two-bit words.

How GZKP generates a proof

GZKP's proof-generation pipeline

  1. Witness in

    An application framework (e.g. xJsnark) compiles the private computation into a circuit and produces the scalar vectors the prover consumes. GZKP slots in below this as a drop-in proving backend.

  2. POLY: shuffle-less NTT new

    Data stays in its global-memory order for the entire transform. Each thread block takes G ≥ 4 SMALL independent groups instead of one big one; the block reads G strided chunks, transposes them inside 48 KB shared memory (the "internal shuffle"), and works on contiguous data from then on. Big integers are stored column-major so a warp reading limb j of 32 consecutive elements touches consecutive addresses.

  3. MSM offline: weighted points new

    Once per circuit, GZKP precomputes weighted copies of each base point (2^tk ⊗ Pᵢ for window offsets t) at checkpoint intervals of M windows — Algorithm 1 tunes M so the table fits any GPU's memory, trading at most (M−1)·k extra point-additions per element.

  4. MSM online: consolidated buckets new

    Using the weighted points, a digit landing in bucket b of ANY window is added to the SINGLE shared bucket b. Point-merging — 90% of MSM time — becomes one big, uniform accumulation, and the serial window-reduction step disappears entirely.

  5. Load-balanced finish new

    Buckets are grouped into similar-sized tasks, scheduled heaviest-first, with warp counts assigned proportional to load; each point-addition itself runs cooperatively across threads. A short bucket-reduction then yields the final point.

  6. Foundation: floating-point field library new

    All field arithmetic packs 52-bit limbs into double-precision floats (15 limbs for 753-bit values), using Dekker's method to recover exact products — exploiting the GPU's abundant FP64 throughput. The library alone speeds up even the BASELINE's design by 1.6×.

From HUST, Tsinghua, and QCRI (ASPLOS 2023). GZKP supports the ALT-BN128 (256-bit), BLS12-381 (381-bit), and MNT4753 (753-bit) curves; each prior GPU prover supported exactly one.

The idea

Don’t move the data — move the work assignment. The NTT insight is that shuffling exists only to make thread accesses contiguous, and contiguity can be manufactured without touching global memory. Later NTT batches consist of many small independent groups; GZKP assigns several of them (G ≥ 4) to one thread block, which loads G strided slices and transposes them in shared memory — 48 KB per SM, free to permute. The global array is written back in the SAME order it was read. What the classic algorithm did with a full-array pass through global memory, GZKP does with an on-chip transpose whose cost is invisible. A column-major layout for the big integers finishes the job: limb j of 32 consecutive elements sits at 32 consecutive addresses, so every warp access coalesces — worth 2× by itself at 753 bits.

Spend cheap preprocessing to delete the serial step. Pippenger’s window-reduction exists because bucket b in window t represents b·2^(t·k)⊗P — different windows carry different weights, so their results must be combined by a doubling chain at the end. GZKP’s move: bake the weight into the POINT instead. If the table already holds 2^(t·k)⊗Pᵢ, then window t’s digit can be added directly into a single, window-agnostic bucket set. All windows merge; the doubling chain vanishes; and point-merging — already 90% of the work — becomes a single uniform phase that saturates the GPU. The naive table is too big (over 5 GB at Zcash’s scale), so GZKP stores weighted points only at checkpoints every M windows and pays at most (M−1)·k extra additions to reach intermediate windows — a knob that adapts the same code to a 32 GB V100 or an 11 GB GTX 1080Ti.

Balance the load you can no longer hide. With one giant merged bucket set, the sparse-scalar imbalance that crippled MINA would be fatal — so GZKP treats bucket sizes as a scheduling input. Buckets are grouped into tasks of similar total load, the heaviest tasks launch first, warps are allocated proportional to load, and individual point-additions are internally parallelized across threads via cooperative groups so even one monster bucket spreads across the machine.

Feed it all with floating-point arithmetic. Field elements are represented as 52-bit limbs stored in FP64 mantissas; Dekker’s method splits each multiplication so products are exact. The GPU’s double-precision units — mostly idle in integer-based provers — do the heavy lifting. This library is orthogonal to the algorithms: the paper shows it accelerates even the baseline’s own design by 1.6×.

Walking through one Zcash proof

A Zcash Sprout proof (2²¹ elements, 381-bit BLS12-381 curve) through GZKP

NTT = finite-field FFT (the POLY stage runs seven). MSM = Σ sᵢ⊗Pᵢ over elliptic-curve points (five per proof). Window/bucket = Pippenger's decomposition of scalars into k-bit digits. Warp = 32 GPU threads executing in lockstep. Timings are from the paper's Tables 3, 5, and 7 on an NVIDIA V100.

Step 1 of 7

The job arrives

The Sprout transaction's circuit yields scalar vectors about 2 million entries long. The prover must run seven NTTs (POLY stage) and five MSMs (MSM stage) over 381-bit field elements. bellperson — the best prior GPU prover for this curve — takes 2.93 s; bellman on a CPU takes 13.7 s.

vector size: 2,097,151
bellperson: 0.69 s POLY + 2.24 s MSM

Why it works

  • Shuffling was pure overhead that scaled with bit-width → keeping data stationary and transposing small tiles in 48 KB shared memory buys contiguity without global traffic → NTT time drops up to 2.1× from this alone, and total NTT runtime becomes linear in N (the baseline’s wasn’t, because its fixed one-group-per-block division left 30 of 32 threads idle in late batches).
  • Window-reduction was serial because windows carried different weights → precomputing the weights into the points makes every window’s contribution commensurable → all additions land in one bucket set, the GPU runs one giant uniform accumulation, and the CPU round-trip for the serial tail disappears.
  • The preprocessing table depends only on the circuit’s fixed base points, not the scalars → it is computed once and amortized over every proof the circuit ever generates → the online path pays only the (M−1)·k extra additions, tunable against available memory.
  • Load imbalance is measurable before launch (bucket sizes are known after digit-counting) → scheduling can be explicit — similar-load groups, heaviest-first, warps proportional to load → sparse real-world scalars no longer serialize the machine, which is precisely where MINA lost its GPU advantage.
  • FP64 units on a V100 vastly outnumber integer-multiply throughput → 52-bit limbs in double mantissas with Dekker’s exact products move field arithmetic onto the abundant resource → 1.6× on the baseline’s own design, compounding with the algorithmic wins.

What it costs

  • Preprocessing memory, deliberately spent. The checkpointed weighted-point table makes GZKP’s BLS12-381 memory footprint larger than bellperson’s (though it plateaus past scale 2²² and stays within a V100’s 32 GB). The paper’s framing: bellperson under-uses GPU memory; GZKP converts that headroom into speed.
  • Offline preprocessing per circuit. The weighted-point table must be built before the first proof. It is reusable across all proofs of that circuit — the standard zkSNARK setting — but a workload with constantly changing base points would not amortize it.
  • Extra additions at checkpoints. Skipping intermediate windows costs up to (M−1)·k point-additions per element — the explicit price of fitting the table into smaller GPUs.
  • Per-curve tuning. The window size k comes from profiling each curve/scale combination, and the finite-field library needs limb-count configuration per bit-width (15 limbs at 753 bits).
  • Engineering depth. Custom CUDA with cooperative groups, a hand-built floating-point field library, and layout changes (column-major limbs) that permeate every kernel — this is a ground-up prover, not a patch to bellperson.

The evaluation

Experiments run on NVIDIA V100 GPUs (with a GTX 1080Ti repeat to check the results aren’t V100-specific), against the strongest available system per curve: libsnark and bellman on CPU, MINA and bellperson on GPU. Real workloads come from xJsnark applications (AES, SHA-256, RSA operations, Merkle trees, an auction) and the Zcash transaction tool — both produce the sparse scalars that stress load balancing — while module microbenchmarks use dense synthetic data, so the sparse-workload wins are not an artifact of cherry-picked inputs. Honest accounting note: the ablations attribute the gains — in the MSM at scale 2²², bucket partitioning alone gives 3.25×, the floating-point library adds 33%, and load balancing brings the total to 5.6×; in the NTT, the library gives 1.6× and the shuffle-less design another 1.5× on top.

Best CPU vs best prior GPU vs GZKP

PropertyBest CPU (libsnark / bellman)Best prior GPU (MINA / bellperson)GZKP
End-to-end zkSNARK proofs, 753-bit curve (6 apps)
Zcash shielded transaction, 381-bit curve
Single NTT operation
Single MSM operation
Elliptic-curve coverage
Single NVIDIA V100 (32 GB, CUDA 11.4) unless noted; multi-GPU test uses four V100s; GTX 1080Ti (11 GB) repeats confirm the ordering on lower-end hardware (NTT avg 8.9×, MSM 4.3–6.1× vs best GPU). End-to-end times sum POLY + MSM stages; MINA end-to-end = libsnark POLY + MINA GPU MSM since MINA accelerates only MSM. Real workloads (xJsnark, Zcash tool) have sparse scalars; module benchmarks use dense synthetic data from libsnark.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. GPU FFT tradition shuffles the array between iteration batches so threads read contiguous memory, and it works beautifully for scientific computing. Predict what happens when the elements grow from 8-byte doubles to 753-bit (~96-byte) field elements.

  2. 2. Pippenger's algorithm ends with window-reduction — combining each window's result with a chain of doublings — which is inherently serial. How does GZKP make this step disappear entirely?

  3. 3. MINA put the MSM stage — over 70% of CPU proving time — on a GPU, yet barely beat libsnark running on CPUs end to end. Why?

  4. 4. GZKP implements 256–753-bit integer arithmetic using double-precision FLOATING-POINT units, packing 52-bit limbs into FP64 mantissas with Dekker's method. Why would exact big-integer math run on float hardware?

Sources

Related papers on Security, Privacy & Trusted Execution

See all 153 →