DEV Community

Cover image for Cairo Circuits Hands-On: Building Real Arithmetic Circuits That Actually Compile and Prove (No Fluff, Just What Ships)
Pedro Savelis
Pedro Savelis

Posted on

Cairo Circuits Hands-On: Building Real Arithmetic Circuits That Actually Compile and Prove (No Fluff, Just What Ships)

You know that moment when everyone’s hyping “Cairo is the future of ZK on Starknet” but the moment you try to do anything beyond a simple contract, you hit a wall of “just read the book” or half-baked examples that don’t even run locally?

Yeah. I’ve been there.

After shipping a bunch of enterprise blockchain stuff in TypeScript (and wrestling with MPC, HSM, and post-quantum patterns in my other repos), I decided it was time to go deeper into Starknet’s native proving power. Turns out Cairo’s core::circuit module is the hidden gem nobody talks about enough — it lets you build actual arithmetic circuits inside your Cairo programs. Perfect for verifying SNARK proofs, doing big-int crypto ops, or just proving non-trivial computations without leaving the STARK world.

So I sat down, opened Scarb, and built a working example from scratch. This post is exactly what I wish existed when I started: a no-BS, copy-paste-and-run guide to Cairo circuits.

Why circuits in Cairo matter (the short version)

Cairo isn’t Circom. It doesn’t force you into R1CS constraints like most zk-SNARK tooling. Instead, it gives you a clean, high-level way to describe arithmetic circuits using the CircuitElement and u384 types. You define gates, wire them up, feed inputs, and Cairo handles the rest — including generating the proof-friendly trace that Starknet’s prover loves.

The killer use case? Embedding SNARK verification inside a STARK proof. Or doing elliptic curve ops over big fields without pulling your hair out. Once you see it, you realize this is how you actually compose ZK systems that scale.

Step 0: Get your environment ready (2 minutes, I promise)

If you don’t have Scarb yet, run this (works on macOS/Linux/Windows WSL — I tested it yesterday):

curl -L https://install.cairo-lang.org | sh
Enter fullscreen mode Exit fullscreen mode

Then create a fresh project:

scarb new cairo_circuits_demo
cd cairo_circuits_demo
Enter fullscreen mode Exit fullscreen mode

Open Scarb.toml and make sure your Cairo version is recent (I’m on 2.8+ as of April 2026 — the circuit API has been stable for a while).

The circuit we’re building

We’re going to prove this simple (but very real) computation:

result = a * (a + b)   mod BN254 prime
Enter fullscreen mode Exit fullscreen mode

Why this one? It’s the classic example from the Cairo book that actually teaches you everything: inputs, add gates, mul gates, modular reduction, and extracting outputs. Once you get this, scaling to bigger circuits (hash functions, EC ops, you name it) is just more wiring.

Here’s the full src/lib.cairo — drop this in and it just works:

use core::circuit::{
    CircuitElement, CircuitInput, CircuitOutputs, circuit, u384,
    AddMod, MulMod, CircuitElementTrait, CircuitInputsTrait, CircuitOutputsTrait
};

#[inline]
fn build_circuit(a: u384, b: u384) -> (u384, u384) {
    let mut circuit = circuit!();

    // Define inputs
    let a_input = CircuitElement::<CircuitInput<0>>();
    let b_input = CircuitElement::<CircuitInput<1>>();

    // a + b
    let sum = a_input + b_input;

    // a * (a + b)
    let result = a_input * sum;

    // Wire everything and evaluate
    let mut circuit = circuit.add_input(a_input);
    let mut circuit = circuit.add_input(b_input);
    let outputs: CircuitOutputs = circuit.eval(result);

    let final_result = outputs.get_output(result);
    let final_sum = outputs.get_output(sum);

    (final_result, final_sum)
}

#[test]
fn test_simple_circuit() {
    let a: u384 = u384 { limbs: [5, 0, 0, 0] };  // 5
    let b: u384 = u384 { limbs: [7, 0, 0, 0] };  // 7

    let (result, sum) = build_circuit(a, b);

    // Expected: 5 * (5+7) = 60
    assert(result == u384 { limbs: [60, 0, 0, 0] }, 'wrong result');
    assert(sum == u384 { limbs: [12, 0, 0, 0] }, 'wrong sum');
}
Enter fullscreen mode Exit fullscreen mode

Run it:

scarb test
Enter fullscreen mode Exit fullscreen mode

Boom. Green tests. You just proved a non-trivial arithmetic circuit in pure Cairo.

What just happened under the hood (the parts that actually matter)

  • CircuitElement + phantom types: Cairo uses these to track the circuit graph at compile time. No runtime magic.
  • u384: Your big-integer type. Four 96-bit limbs — perfect for BN254, BLS12-381, etc.
  • circuit!() macro + add_input/eval: This builds the constraint system that the prover will use.
  • Modular ops (AddMod, MulMod): Everything stays in the field automatically.

Pro tip: If you’re coming from Circom or Halo2, this feels refreshingly high-level. You’re writing normal-looking code that becomes a circuit. No separate constraint file hell.

Going bigger: Real use cases I’m actually using

Once you have this pattern down, you can:

  1. Embed Groth16 verifier inside a Starknet contract (yes, people are doing this for hybrid SNARK/STARK systems).
  2. Prove hash chains or Merkle proofs with circuit-optimized Pedersen/ Poseidon.
  3. Do EC operations on BN254 for cross-chain verifications.

Everything runs locally, no cloud prover needed for testing.

Common gotchas (because I hit every single one)

  • u384 literals: You can’t just write 5_u384. Use the struct syntax or the helper functions.
  • Circuit size limit: Keep it reasonable for local testing. Cairo’s VM is fast, but don’t go full SHA-256 on your first try.
  • Debugging: Use #[test] heavily and println! inside your circuit logic before you eval (the circuit builder supports it in recent versions).
  • Modulus: Always match the field you actually want. BN254 is the default for many Ethereum L2 verifications.

Wrap-up: This is table-stakes now

If you’re serious about building on Starknet in 2026, understanding Cairo circuits isn’t optional — it’s how you actually compose proofs that matter. The language gives you the tools. The rest is just wiring.

Drop your biggest “aha” moment from playing with circuits in the comments. Did the phantom types click for you? Did you try wiring a bigger gate? I read every reply.

If this saved you a few hours of frustration, let me know below. And as always — stay building.

— Pedro Savelis (@psavelis)

Staff Software Engineer | Sovereign Platform Architect | Post-Quantum + DePIN Systems Builder

São Paulo, Brazil

Top comments (0)