Witness & Circuit I/O
The compiled Circuit<Field> knows how many input wires it wants (circuit->ninputs) and how many of those are public (circuit->npub_in). The prover’s job is to fill a Dense<Field> of the right shape with values in exactly the order the wires were declared during circuit construction. The verifier fills a second, smaller Dense<Field> with just the public portion. This page covers how to shape those containers and how to get real-world bytes into them.
The two Dense containers
W— the full witness. Shape(nc, circuit->ninputs). Every wire, public first, private after.ncis the batching factor frommkcircuit(nc); use1unless you are batching independent statements.pub— just the public portion, firstnpub_inwires. Must agree with the firstnpub_invalues inW.
Both are Dense<Field> (see Arrays); both are filled index-driven rather than name-driven. The order is the order you called lc.eltw_input() during circuit construction — not alphabetical, not grouped-by-logical-role. If your circuit declared inputs in the order [one, pk_x, pk_y, msg_bytes..., sig_r, sig_s], that is the exact order you fill.
Filling with DenseFiller
DenseFiller<Field> is a tiny helper that walks a Dense<Field> with n0 == 1 and pushes one element at a time:
Dense<Field> W(1, circuit->ninputs);
DenseFiller<Field> filler(W);
filler.push_back(field.one()); // wire 0
filler.push_back(pk_x);
filler.push_back(pk_y);
// ... public inputs in declaration order, total npub_in values
// ... then private inputs in declaration orderTwo conventions worth internalising:
- Wire 0 is always
field.one(). The compiler uses it as the multiplicative identity wire; every circuit that declares any public input starts by declaringoneaslc.eltw_input(). Skipping it silently desynchronises every subsequent wire. - Public-then-private, no gaps. The
private_input()call during circuit construction setnpub_into the number of public wires seen so far. If your fill order swaps a public and a private, the verifier’spubcontainer does not match the firstnpub_inofWand the proof fails.
Ingesting real-world bytes
The gap that trips people up most is going from “I have a 32-byte SHA-256 digest” to “here is a field element the circuit can consume.” There are two idioms.
One field element per byte (simple, fewer witness elements)
Each byte becomes one field element in the low 8 bits. When you need a bit-level view of a known constant byte (e.g., a fixed tag or length prefix), you can use lc.vbit8(x) — but note that vbit8(uint64_t x) takes a compile-time integer literal and returns a constant v8; it does not decompose a runtime circuit wire. To decompose a runtime witness byte into its 8 bits, declare each bit as a separate witness wire with lc.input() and range-check that they are genuine bits with lc.assert_is_bit(...). Use the one-field-element-per-byte layout when bytes are incidental (a fixed-value tag you intend to match against a constant), not when you need runtime bit arithmetic on the byte.
One field element per bit (dense, preferred for bulk hashed data)
Hashes, signatures, and large payloads are packed as bit-decompositions — one field element per bit rather than per byte. The canonical pattern, lifted from flatsha256_circuit_test.cc:
// Off-circuit: one field element per bit of the byte.
for (int i = 0; i < 8; ++i) {
filler.push_back(field.of_scalar((byte >> i) & 1));
}Inside the circuit, the prover wires these bits into bitvec<8> and the gadget calls lc.as_scalar(bits) — which re-packs them as \sum_i b_i \cdot 2^i — wherever arithmetic needs the byte as a number. The bit representation is the common currency for hash, MAC, and byte-comparison gadgets; it is also what the compiler’s CSE is best at deduplicating.
For a full 32-byte digest that is 256 field elements of witness — not 32. The witness count feels high; in practice the overall circuit is smaller because gadgets share bit wires and avoid the per-byte runtime bit-decomposition gates that the one-field-element-per-byte layout would otherwise require.
Exposing outputs
There is no separate “public output” container. Output wires are declared during circuit construction via lc.output(wire, index) (defined on the Logic object as void output(const EltW& x, size_t i)), which forwards to the backend’s output_wire. These wires become part of the sumcheck claim: the prover commits to their values and the verifier checks that the circuit’s last-layer polynomial evaluates to those values at the claimed points. In practice the pattern is:
- During circuit construction, call
lc.output(wire, index)for each output wire you want to expose. - The compiled
Circuit<Field>carriesnv(output count) and the layer structure required to recover them. - The caller supplies the claimed output value to the verifier alongside the public input — either as a separate argument or by extending the public-input list.
See zk_test.cc in the upstream repo for a worked example; the exact API surface lives on Proof System: ZK.
Pitfalls to watch for
- Forgetting
field.one()at wire 0. Every downstream wire is off by one. The proof compiles and runs; verification fails with a transcript mismatch that looks mysterious. - Re-using a
DenseFilleracrosscommitandprove. The filler advances a cursor. Once you have writtenWonce, stop touching it. - Confusing
nc > 1with re-proving.nc > 1batches independent instances in a single proof. Re-runningcommitwith a fresh witness produces a new proof with its own transcript and randomness.
Related
- Arrays (Reference) — the
Dense<Field>container in full. - Designing a Circuit — the preceding guide on field and public/private choices.
- End-to-end Prover & Verifier — how
Wandpubflow into the prover and verifier.