# Generación de pruebas de conocimiento cero acelerada por GPU

> Las pruebas de conocimiento cero permiten cómputo verificable sin exponer transacciones privadas, y se convierten en una pieza poderosa para transacciones que preservan la privacidad y para la verificación onchain escalable.

Por Shodipo Ayomide, publicado el 7 sept 2025. Versión web: https://blog.shodipoayomide.com/es/gpu-accelerated-zero-knowledge-proving

![Generación de pruebas de conocimiento cero acelerada por GPU](https://blog.shodipoayomide.com/banners/GPU-Accelerated-Zero-Knowledge-Proving.png)

Las pruebas de conocimiento cero permiten cómputo verificable sin exponer transacciones privadas, y se convierten en una pieza poderosa para transacciones que preservan la privacidad y para la verificación onchain escalable. Sin embargo, la intensidad computacional de la generación de pruebas ha sido, con el tiempo, un problema importante. Un tradeoff muy conocido en los sistemas ZK es el del "prover hambriento de recursos": conseguir una prueba pequeña y rápida de verificar requiere mucho cómputo y a menudo es bastante lento, lo que bloquea el uso en tiempo real.

Las GPUs modernas cambian drásticamente esta ecuación. Aprovechando su diseño paralelo, convierten lo que era un problema de fondo en un problema de ingeniería que se puede resolver. Los sistemas ZK de hoy usan principalmente dos construcciones: `zk-SNARKs` y `zk-STARKs`, y ambas dependen de operaciones centrales que encajan perfectamente con la aceleración por GPU.

## ¿Qué es lo que realmente tarda tanto?

Casi todo el tiempo de generación de pruebas se va en dos operaciones: Multi-Scalar Multiplication (MSM) y Number Theoretic Transform (NTT).

### ¿Qué es Multi-Scalar Multiplication?

MSM calcula la suma de puntos de curva elíptica escalados: $S = \sum_{i=1}^{n} a_i \cdot P_i$. Esta sola operación consume nada menos que el `60-80%` del tiempo de generación de pruebas en los `SNARKs`. Por sí misma requiere una gran cantidad de accesos aleatorios a memoria para traer los puntos de la curva elíptica, lo que convierte al ancho de banda de memoria en el principal problema de rendimiento aquí. El algoritmo de Pippenger paraleliza esta tarea repartiendo los escalares en buckets.

```rust
fn pippenger_msm<G: CurveGroup>(
    bases: &[G::Affine],
    scalars: &[G::ScalarField],
    window_size: usize,
) -> G {
    let num_buckets = 1 << window_size;
    let mut buckets = vec![G::zero(); num_buckets];
    
    for (base, scalar) in bases.iter().zip(scalars.iter()) {
        let bucket_index = extract_window(scalar, window_size);
        buckets[bucket_index] += base;
    }
    
    // bucket aggregation
    buckets.into_iter()
           .enumerate()
           .map(|(i, bucket)| bucket * G::ScalarField::from(i))
           .fold(G::zero(), |acc, term| acc + term)
}
```

Entonces, lo que hace la implementación en GPU es repartir esto entre miles de hilos, logrando más de `700 million` operaciones de puntos por segundo en una `RTX 4090`.

### ¿Qué es Number Theoretic Transform?

NTT, por otro lado, multiplica polinomios rápido y, a diferencia de los saltos aleatorios en memoria de MSM, NTT sigue un patrón predecible, justo para lo que están hechas las GPUs.

```c
__global__ void ntt_radix2_kernel(
    uint64_t* coeffs,
    uint64_t* twiddles,
    uint32_t stage,
    uint32_t n
) {
    uint32_t tid = threadIdx.x + blockIdx.x * blockDim.x;
    uint32_t stride = 1 << stage;
    uint32_t m = n >> (stage + 1);
    
    if (tid >= m) return;
    
    uint32_t k = tid % stride;
    uint32_t j = tid / stride;
    uint32_t idx1 = j * (stride << 1) + k;
    uint32_t idx2 = idx1 + stride;
    
    uint64_t u = coeffs[idx1];
    uint64_t v = mod_mul(coeffs[idx2], twiddles[k], MODULUS);
    
    coeffs[idx1] = mod_add(u, v, MODULUS);
    coeffs[idx2] = mod_sub(u, v, MODULUS);
}
```

Este patrón secuencial permite que las implementaciones de NTT de alto rendimiento alcancen un `85-95%` del ancho de banda de memoria.

## Caso de estudio: rendimiento de pruebas en CPU vs GPU

Piensa en Alice y Bob, que operan `zkEVM Sequencers` para sus respectivas cadenas `Layer-2`. Procesan lotes de transacciones idénticos, pero usan infraestructura de pruebas distinta.

### Escenario: generación de pruebas de bloques en un zkEVM

Ambos sequencers deben probar un bloque que contiene `500` transacciones con las siguientes operaciones:

- **1,200** transferencias ERC-20
- **300** swaps en Uniswap V3
- **200** actualizaciones de almacenamiento
- **100** despliegues de contratos

Esto genera un circuito con aproximadamente $2^{26}$ restricciones, algo típico para un bloque de `zkEVM` de tamaño medio.

### El enfoque de Alice con CPU

Alice corre su prover en una `CPU` de gama alta - `AMD EPYC 7763, 64 cores`:

```rust
impl CpuZkEvmProver {
    fn prove_block(&self, block: &Block) -> ProofResult {
        let start = Instant::now();
        
        // circuit compilation
        let circuit = self.compile_block_circuit(block); // 45s
        
        // msm operations
        let witness_commitment = self.cpu_msm(&circuit.witnesses); // 180s
        
        // ntt operations
        let constraint_proof = self.cpu_ntt(&circuit.constraints); // 90s
        
        // final proof
        let proof = self.assemble_proof(witness_commitment, constraint_proof);
        
        ProofResult {
            proof,
            proving_time: start.elapsed(), // ~315s total
            memory_usage: 45_000_000_000, // 45GB peak
        }
    }
    
    fn cpu_msm(&self, witnesses: &[Fr]) -> G1Projective {
        witnesses.iter()
                .zip(self.setup.iter())
                .map(|(scalar, base)| base.mul(scalar))
                .fold(G1Projective::identity(), |acc, point| acc + point)
    }
}
```

Resultados de Alice:

- **Tiempo de generación de la prueba:** 5.25 minutos por bloque
- **Uso de memoria:** 45GB de pico
- **Throughput:** ~95 transacciones/minuto probadas
- **Costo del hardware:** servidor de $8,000

### El enfoque de Bob acelerado por GPU

Bob usa una `RTX 4090` con aceleración de `ICICLE`:

```rust
use icicle_bls12_381::{curve::G1Projective, msm};
use icicle_core::msm::MSMConfig;

impl GpuZkEvmProver {
    fn prove_block(&self, block: &Block) -> ProofResult {
        let start = Instant::now();
        
        // circuit compilation
        let circuit = self.compile_block_circuit_gpu(block); // 12s
        
        // gpu msm - 16,384 cores parallel
        let witness_commitment = self.gpu_msm(&circuit.witnesses); // 8s
        
        // gpu ntt
        let constraint_proof = self.gpu_ntt(&circuit.constraints); // 3 seconds
        
        // final proof
        let proof = self.assemble_proof(witness_commitment, constraint_proof);
        
        ProofResult {
            proof,
            proving_time: start.elapsed(), // ~23s total
            memory_usage: 18_000_000_000, // 18GB peak
        }
    }
    
    fn gpu_msm(&self, witnesses: &[Fr]) -> Result<G1Projective> {
        let cfg = MSMConfig {
            precompute_factor: 8,
            c: 16,
            ..Default::default()
        };
        
        msm::msm(witnesses, &self.gpu_setup, &cfg)
    }
}
```

Resultados de Bob:

- **Tiempo de generación de la prueba:** 23 segundos por bloque
- **Uso de memoria:** 18GB de pico
- **Throughput:** ~1,300 transacciones/minuto probadas
- **Costo del hardware:** $1,800 (RTX 4090 + servidor básico)

### Comparando los resultados

| Operación | Alice (CPU) | Bob (GPU) | Aceleración |
|-----------|-------------|-----------|---------|
| Compilación del circuito | 45s | 12s | 3.8× |
| Operaciones MSM | 180s | 8s | **22.5×** |
| Operaciones NTT | 90s | 3s | **30×** |
| **Prueba total del bloque** | **315s** | **23s** | **13.7×** |

### Por qué esto lo cambia todo

Finalidad de la red:

- Red de Alice: finalidad de 5.25 minutos para las confirmaciones en L1
- Red de Bob: finalidad de 23 segundos para las confirmaciones en L1

#### Costo por transacción

```rust
// one every 12s
const BLOCKS_PER_DAY: u32 = 7200;

impl ProvingEconomics {
    fn daily_capacity(&self, proving_time_seconds: u32) -> u32 {
        let daily_seconds = 24 * 60 * 60;
        daily_seconds / proving_time_seconds
    }
}
```

Capacidad diaria de generación de pruebas (Alice vs Bob):

- Alice puede probar: 274 bloques/día, es decir, 137,000 transacciones
- Bob puede probar: 3,756 bloques/día, es decir, 1,878,000 transacciones

#### Lo que Alice necesita para competir

Alice necesitaría 14 CPUs para igualar la configuración de una sola GPU de Bob, lo que implica:

- 14× más costo en hardware - \$112,000 vs \$8,000
- 14× más consumo de energía - 14kW vs 1kW
- Sobrecarga de coordinación distribuida compleja

### Cómo se lee la memoria

La diferencia de rendimiento nace, en el fondo, de cómo cada servidor accede a la memoria:

```rust
// alice's cpu msm - sequential, cache-bound
for (scalar, base) in witnesses.iter().zip(setup.iter()) {
    result += base * scalar; // cache miss every iteration
}

// bob's gpu msm - parallel, lightweight
__global__ void msm_kernel(scalar_t* scalars, point_t* bases, point_t* buckets) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    int bucket_idx = extract_window(scalars[tid], WINDOW_SIZE);
    atomicAdd(&buckets[bucket_idx], bases[tid]); // coalesced memory access
}
```

Este caso de estudio, por ejemplo, simplemente muestra por qué la aceleración por GPU se ha vuelto esencial para los despliegues de `zkEVM` en producción. La infraestructura de Bob puede atender a `13.7×` más usuarios mientras mantiene una finalidad más rápida y costos operativos más bajos.

## Patrones de aceleración en SNARK vs. STARK

### Sistemas SNARK

`Groth16` y `PLONK` dependen mucho de MSM. `Groth16`, por ejemplo, dedica alrededor del `80%` de su tiempo de generación de pruebas a operaciones de curva elíptica. `PLONK`, en cambio, reparte la carga de forma más equilibrada entre MSM `~60%` y operaciones polinómicas `~30%`, por lo que requiere una optimización balanceada para ambas.

```rust
impl<E: Pairing> Groth16Prover<E> {
    fn create_proof(&self, circuit: &Circuit<E::ScalarField>) -> Result<Proof<E>> {
        let (a, b, c) = circuit.to_matrices();
        
        let g_a = self.params.alpha_g1.msm(&a);
        let g_b = self.params.beta_g2.msm(&b);  
        let g_c = self.params.gamma_g1.msm(&c);
        
        Ok(Proof { a: g_a, b: g_b, c: g_c })
    }
}
```

### Sistemas STARK

Los `STARKs` están dominados por la aritmética polinómica `NTTs` y el hashing, por ejemplo `Poseidon` o `Blake3`. Por lo regulares que son los patrones de acceso a memoria en los `STARKs`, suelen aprovechar mejor el ancho de banda de memoria de la GPU en comparación con los patrones de acceso más aleatorios de las operaciones MSM de los `SNARK`.

```rust
impl<F: PrimeField> StarkProver<F> {
    fn prove(&self, trace: &ExecutionTrace<F>) -> StarkProof<F> {
        // polynomial interpolation via ntt
        let trace_poly = self.interpolate(trace);
        
        let composition_poly = self.evaluate_constraints(&trace_poly);
        
        let fri_proof = self.fri_commit(&composition_poly);
        
        StarkProof {
            trace_commitment: self.commit(&trace_poly),
            composition_commitment: self.commit(&composition_poly),
            fri_proof,
        }
    }
}
```

## Aceleración de zkEVM

Los zkEVMs presentan retos únicos por la complejidad de las instrucciones de la `EVM` y por las grandes representaciones de estado. El proceso de generación de pruebas implica generar restricciones para cada opcode de la `EVM` y cada transición de estado.

### Probando la EVM

```rust
impl ZkEvmProver {
    fn prove_block(&self, block: &Block) -> ZkEvmProof {
        let mut circuit = EvmCircuit::new();
        
        for tx in &block.transactions {
            for (pc, opcode) in tx.trace.iter().enumerate() {
                match opcode {
                    OpCode::ADD => {
                        let (a, b) = self.stack.pop2();
                        let result = a + b;
                        circuit.add_constraint(a + b - result);
                        self.stack.push(result);
                    },
                    OpCode::SSTORE => {
                        let (key, value) = self.stack.pop2();
                        circuit.add_storage_constraint(key, value);
                        self.update_state_root(key, value);
                    },
                    // ...
                }
            }
        }
        
        self.prove_circuit(circuit)
    }
}
```

Cada opcode, desde **ADD** hasta **SSTORE**, debe traducirse en un conjunto de restricciones polinómicas.

### Probando el árbol de estado

Por lo general, los zkEVMs siempre deben probar que los patrones de acceso a memoria y las actualizaciones de almacenamiento son correctos. Esto normalmente implica operaciones sobre un `Merkle tree` para verificar el state root

```rust
fn constrain_merkle_update(
    circuit: &mut Circuit,
    old_root: Variable,
    new_root: Variable,
    key: Variable,
    old_value: Variable,
    new_value: Variable,
    path: &[Variable],
) {
    let mut current = old_value;
    let mut updated = new_value;
    
    for (i, &sibling) in path.iter().enumerate() {
        let bit = circuit.extract_bit(key, i);
        
        let hash_input = circuit.select(bit, (sibling, current), (current, sibling));
        current = circuit.poseidon_hash(hash_input.0, hash_input.1);
        
        let updated_input = circuit.select(bit, (sibling, updated), (updated, sibling));
        updated = circuit.poseidon_hash(updated_input.0, updated_input.1);
    }
    
    circuit.assert_equal(current, old_root);
    circuit.assert_equal(updated, new_root);
}
```

La aceleración por GPU suele volverse importante para las extensas operaciones de hashing que requieren las verificaciones de transiciones de estado

## Conclusión

Las GPUs no solo aceleraron la generación de pruebas ZK, básicamente cambiaron la forma en que se hace, al resolver el problema del "prover hambriento de recursos". Ahora tenemos Layer 2s que finalizan en segundos y DeFi privado corriendo en producción, cosas que antes no eran del todo posibles. A medida que la complejidad de los circuitos aumenta con aplicaciones como los `zkEVMs`, la aceleración por GPU ya no es solo una cuestión de optimización, es un requisito fundamental para los despliegues en producción.

### Enlaces esenciales

  * **ICICLE:** Una librería de GPU open-source de Ingonyama para acelerar primitivas ZK. [GitHub](https://github.com/ingonyama-zk/icicle)
  * **Arkworks:** Un ecosistema basado en Rust para diseñar e implementar sistemas de pruebas ZK. [Sitio web](https://arkworks.rs/)
  * **Halo2:** Un sistema de pruebas basado en PLONK desarrollado por la Zcash Foundation, muy usado en proyectos como el PSE zkEVM. [GitHub](https://github.com/zcash/halo2)
  * **ZPrize:** Una competencia de la industria enfocada en acelerar el rendimiento ZK mediante software y hardware. [Sitio web](https://www.zprize.io/)
  * "zk-SNARKs: A Primer" de Vitalik Buterin: Una excelente explicación de alto nivel sobre cómo se construyen los zk-SNARKs. [Artículo](https://vitalik.eth.limo/general/2021/01/26/snarks.html)
  * "Anatomy of a STARK" de StarkWare: Un análisis a fondo de los componentes matemáticos de los zk-STARKs.[Artículo](https://medium.com/starkware/stark-math-the-journey-begins-51bd2b063c71)
  * "Introducing ICICLE: An Open-Source GPU Library for Zero Knowledge Acceleration" de Ingonyama [Artículo](https://www.ingonyama.com/post/introducing-icicle-an-open-source-gpu-library-for-zero-knowledge-acceleration)
  * "Blitzar, A novel opensource zero-knowledge proof for SQL operations" de Space and Time. Blitzar ofrece un framework más rápido y robusto para construir pruebas ZK aceleradas por GPU [Github](https://github.com/spaceandtimefdn/blitzar)
