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.
Fp256for ECDSA oversecp256r1,Fp256k1forsecp256k1,Fp384/Fp521for 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_2of the circuit’s depth. Drives the number of sumcheck rounds; largernlmeans longer proofs and longer verification.circuit.nterms()— total quadratic terms summed across all layers (vialayer.nterms()per layer). Drives prover time: the sumcheck inner loop does work proportional to this.- Per-layer wire count (
layer.nw) — each layer’sDensewitness holdsnw · ncfield elements at commit time. There is no singlenwiresfield; inspectcircuit.l[i].nwper layer. nc(copy count) — the batching factor fromQ.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.
What to read next
- Witness & Circuit I/O — the concrete layout of the
Densewitness the compiler’sCircuitexpects. - Verifying a Signed Document — the canonical attestation-shaped composition, end to end.
- Circuits (Reference) — the full gate library.
- Proof System: ZK — what the compiled circuit feeds into.