Skip to Content
⚠️ This documentation is AI-generated, for personal use only, and is not supported or endorsed by Google.
GuidesDesigning a Circuit

Designing a Circuit

A Longfellow proof starts with three decisions: the field your statement lives in, the split between public input and witness, and the shape of the arithmetic circuit that expresses the statement. Get those three right and everything downstream — the prover’s cost, the verifier’s cost, the proof size — falls out. Get them wrong and you will find yourself rewriting the circuit a round later. This page is the decision framework; the Reference pages linked below carry the full APIs.

The three decisions

Field choice

The field is the alphabet the prover and verifier both speak. Two families matter:

  • Prime fields. Fp256 for ECDSA over secp256r1, Fp256k1 for secp256k1, Fp384 / Fp521 for higher-security curves. Pick the one that matches the elliptic curve in your signature scheme — the in-circuit ECDSA gadget only works when the coordinate field matches the curve’s base field. See Prime Fields for the menu.
  • Binary fields. GF(2^{128}) for polynomial MACs, bit-heavy parsing, or anywhere XOR is the natural operation. See Binary Fields.

When in doubt: Fp256 is the right answer for most attestation-shaped statements.

Public-input vs. witness split

Every wire in a Longfellow circuit is either public (known to the verifier before the proof runs) or private (known only to the prover). The split is a commitment: public inputs cannot be hidden later, and private inputs must be consistent with every subsequent gate. The rule of thumb is simple — anything the verifier already has, make public; everything else, put in the witness. Public keys, field moduli, bounds on lengths, fixed literals all belong in public input. The document being attested, its hash preimage, signature components, and ephemeral values belong in the witness.

The split is declared inline as you emit inputs. Public inputs come first; a single call to Q.private_input() marks the boundary, after which every lc.eltw_input() creates a witness wire:

QuadCircuit<Field> Q(field); CompilerBackend<Field> cbk(&Q); Logic<Field, CompilerBackend<Field>> lc(&cbk, field); auto one = lc.eltw_input(); // public auto pk_x = lc.eltw_input(); // public auto pk_y = lc.eltw_input(); // public Q.private_input(); // boundary — npub_in captured here auto msg_byte0 = lc.eltw_input(); // private // ...

Q.private_input() is a one-shot: call it once, right after the last public input. Calling it more than once is not a compile-time error, but it violates the protocol invariant — only the first call sets npub_in, and a second call will corrupt the public/private boundary.

Size and proof-time tradeoffs

The compiled Circuit<Field> exposes shape constants that together determine cost:

  • nl (layer count) — roughly \log_2 of the circuit’s depth. Drives the number of sumcheck rounds; larger nl means longer proofs and longer verification.
  • circuit.nterms() — total quadratic terms summed across all layers (via layer.nterms() per layer). Drives prover time: the sumcheck inner loop does work proportional to this.
  • Per-layer wire count (layer.nw) — each layer’s Dense witness holds nw · nc field elements at commit time. There is no single nwires field; inspect circuit.l[i].nw per layer.
  • nc (copy count) — the batching factor from Q.mkcircuit(nc). Amortises constant overheads when you are proving many independent instances.

The first question to ask when a prove is slow is which of these grew? Deep pipelines (long SHA-256 chains, long ECDSA scalar loops) bump nl. Wide pipelines (lots of parallel bytes) bump circuit.nterms() without bumping nl. Byte-heavy parsing often bumps per-layer wire counts faster than you expect — that is where the bit-decomposition → byte-pack pattern in the Witness & Circuit I/O guide earns its keep.

Last updated on