LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
Wtns.cpp
Go to the documentation of this file.
1//===-- Wtns.cpp - snarkjs-compatible witness output ------------*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2026 Project LLZK
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
9
10#include "Wtns.h"
11
12#include "Errors.h"
13#include "WitnessSelection.h"
14#include "r1cs/Dialect/IR/Ops.h"
15#include "r1cs/Transforms/TransformationPassPipelines.h"
16
21
22#include <mlir/Pass/PassManager.h>
23#include <mlir/Support/FileUtilities.h>
24
25#include <llvm/ADT/SmallVector.h>
26#include <llvm/ADT/StringExtras.h>
27#include <llvm/Support/ToolOutputFile.h>
28
29#include <climits>
30#include <cstdint>
31#include <limits>
32
33using namespace mlir;
34
35namespace llzk::witgen {
36namespace {
37
38constexpr char WTNS_MAGIC[] = {'w', 't', 'n', 's'};
39constexpr uint32_t WTNS_VERSION = 2;
40constexpr uint32_t WTNS_SECTION_COUNT = 2;
41constexpr uint32_t WTNS_HEADER_SECTION = 1;
42constexpr uint32_t WTNS_VALUES_SECTION = 2;
43constexpr uint32_t WTNS_FIELD_LIMB_BITS = 64;
44constexpr uint32_t WTNS_FIELD_LIMB_BYTES = WTNS_FIELD_LIMB_BITS / CHAR_BIT;
45
46void appendSection(BinaryBuffer &file, uint32_t type, const BinaryBuffer &section) {
47 file.writeU32(type);
48 file.writeU64(section.size());
49 file.writeBytes(section.bytes());
50}
51
52llvm::Expected<llvm::DynamicAPInt>
53readFelt(const llvm::json::Object &object, StringRef key, const Field &field) {
54 const llvm::json::Value *json = object.get(key);
55 if (!json) {
56 return makeError(llvm::Twine("full witness is missing '") + key + "'");
57 }
58 std::optional<StringRef> text = json->getAsString();
59 if (!text || text->empty() || !llvm::all_of(*text, llvm::isDigit)) {
60 return makeError(llvm::Twine("full witness value '") + key + "' is not a non-negative integer");
61 }
62 llvm::DynamicAPInt value = toDynamicAPInt(*text);
63 if (value >= field.prime()) {
64 return makeError(llvm::Twine("full witness value '") + key + "' is outside the field");
65 }
66 return value;
67}
68
69llvm::Expected<OwningOpRef<ModuleOp>> lowerModuleToR1CS(ModuleOp moduleOp) {
70 OwningOpRef<ModuleOp> lowered = cast<ModuleOp>(moduleOp->clone());
71 PassManager pm(lowered->getContext());
72 r1cs::buildFullR1CSLoweringPipeline(pm);
73 if (failed(pm.run(*lowered))) {
74 return makeError("failed to lower a module clone while validating .wtns wire ordering");
75 }
76 return lowered;
77}
78
79llvm::Expected<size_t> getR1CSWireCount(ModuleOp r1csModule, StringRef circuitName) {
80 auto circuit = r1csModule.lookupSymbol<r1cs::CircuitDefOp>(circuitName);
81 if (!circuit) {
82 return makeError("R1CS lowering did not produce a circuit for the llzk.main struct");
83 }
84
85 Block &entry = circuit.getBody().front();
86 size_t wireCount = 1 + entry.getNumArguments();
87 wireCount += llvm::range_size(entry.getOps<r1cs::SignalDefOp>());
88 return wireCount;
89}
90
91llvm::Expected<std::string> getMainStructName(ModuleOp moduleOp) {
92 SymbolTableCollection tables;
93 auto mainDef = getMainInstanceDef(tables, moduleOp.getOperation());
94 if (failed(mainDef) || !mainDef.value()) {
95 return makeError("module is missing a concrete llzk.main struct");
96 }
97 return mainDef->get().getSymName().str();
98}
99
100llvm::Expected<SmallVector<llvm::DynamicAPInt>>
101collectWitnessValues(ModuleOp moduleOp, const llvm::json::Value &fullWitness, const Field &field) {
102 const auto *root = fullWitness.getAsObject();
103 const auto *inputs = root ? root->getObject("inputs") : nullptr;
104 const auto *signals = root ? root->getObject("signals") : nullptr;
105 if (!inputs || !signals) {
106 return makeError(".wtns output requires a full-witness llzk-witgen result");
107 }
108
109 SymbolTableCollection tables;
110 auto mainDef = getMainInstanceDef(tables, moduleOp.getOperation());
111 if (failed(mainDef) || !mainDef.value()) {
112 return makeError("module is missing a concrete llzk.main struct");
113 }
114 auto compute = mainDef->get().getComputeFuncOp();
115 auto constrain = mainDef->get().getConstrainFuncOp();
116 if (!compute) {
117 return makeError("main struct is missing @compute");
118 }
119 if (!constrain || constrain.getNumArguments() != compute.getNumArguments() + 1) {
120 return makeError("main @constrain inputs do not match @compute inputs");
121 }
122
123 SmallVector<llvm::DynamicAPInt> witness;
124 // Wire 0 is the implicit constant-one wire in R1CS and therefore the first
125 // value in the corresponding snarkjs witness.
126 witness.push_back(field.one());
127 auto appendMemberClass = [&](bool isPublic) -> llvm::Error {
128 for (component::MemberDefOp member : mainDef->get().getMemberDefs()) {
129 if (member.hasPublicAttr() != isPublic) {
130 continue;
131 }
132 if (!isa<felt::FeltType>(member.getType())) {
133 return makeError(".wtns output currently requires scalar felt main members");
134 }
135 auto value = readFelt(*signals, member.getSymName(), field);
136 if (!value) {
137 return value.takeError();
138 }
139 witness.push_back(*value);
140 }
141 return llvm::Error::success();
142 };
143 auto inputBindings = collectInputBindings(compute);
144 auto appendInputClass = [&](bool isPublic) -> llvm::Error {
145 for (const InputBinding &binding : inputBindings) {
146 // R1CS lowering derives input visibility from constrain(), whose first
147 // argument is self and whose remaining arguments correspond to compute().
148 if (constrain.hasArgPublicAttr(binding.index + 1) != isPublic) {
149 continue;
150 }
151 if (!isa<felt::FeltType>(binding.type)) {
152 return makeError(".wtns output currently requires scalar felt main inputs");
153 }
154 auto value = readFelt(*inputs, binding.name, field);
155 if (!value) {
156 return value.takeError();
157 }
158 witness.push_back(*value);
159 }
160 return llvm::Error::success();
161 };
162
163 if (auto error = appendMemberClass(true)) {
164 return error;
165 }
166 if (auto error = appendInputClass(true)) {
167 return error;
168 }
169 if (auto error = appendInputClass(false)) {
170 return error;
171 }
172 if (auto error = appendMemberClass(false)) {
173 return error;
174 }
175
176 return witness;
177}
178
179llvm::Expected<BinaryBuffer>
180serializeWtns(ArrayRef<llvm::DynamicAPInt> witness, const Field &field) {
181 if (witness.size() > std::numeric_limits<uint32_t>::max()) {
182 return makeError("witness length does not fit in the .wtns header");
183 }
184 uint64_t fieldSizeWide = ((static_cast<uint64_t>(field.bitWidth()) + WTNS_FIELD_LIMB_BITS - 1) /
185 WTNS_FIELD_LIMB_BITS) *
186 WTNS_FIELD_LIMB_BYTES;
187 if (fieldSizeWide > std::numeric_limits<uint32_t>::max()) {
188 return makeError("field encoding size overflows the .wtns header");
189 }
190 uint32_t fieldSize = static_cast<uint32_t>(fieldSizeWide);
191
192 BinaryBuffer header;
193 header.writeU32(fieldSize);
194 if (auto error = header.writeFieldElement(fieldSize, field.prime())) {
195 return error;
196 }
197 header.writeU32(checkedCast<uint32_t>(witness.size()));
198
199 BinaryBuffer values;
200 for (const llvm::DynamicAPInt &value : witness) {
201 if (value < 0 || value >= field.prime()) {
202 return makeError("witness field element is not a canonical representative");
203 }
204 if (auto error = values.writeFieldElement(fieldSize, value)) {
205 return error;
206 }
207 }
208
209 BinaryBuffer file;
210 file.writeBytes(WTNS_MAGIC);
211 file.writeU32(WTNS_VERSION);
212 file.writeU32(WTNS_SECTION_COUNT);
213 appendSection(file, WTNS_HEADER_SECTION, header);
214 appendSection(file, WTNS_VALUES_SECTION, values);
215 return file;
216}
217
218llvm::Error writeBinaryFile(StringRef outputFilename, const BinaryBuffer &file) {
219 std::unique_ptr<llvm::ToolOutputFile> output = openOutputFile(outputFilename);
220 if (!output) {
221 return makeError(llvm::Twine("failed to open .wtns output: ") + outputFilename);
222 }
223 output->os().write(file.bytes().data(), file.bytes().size());
224 output->os().flush();
225 if (output->os().has_error()) {
226 return makeError(llvm::Twine("failed to write .wtns output: ") + outputFilename);
227 }
228 output->keep();
229 return llvm::Error::success();
230}
231
232} // namespace
233
234llvm::Error writeWtns(
235 ModuleOp moduleOp, const llvm::json::Value &fullWitness, const Field &field,
236 StringRef outputFilename
237) {
238 auto mainName = getMainStructName(moduleOp);
239 if (!mainName) {
240 return mainName.takeError();
241 }
242
243 auto witness = collectWitnessValues(moduleOp, fullWitness, field);
244 if (!witness) {
245 return witness.takeError();
246 }
247
248 auto loweredModule = lowerModuleToR1CS(moduleOp);
249 if (!loweredModule) {
250 return loweredModule.takeError();
251 }
252 auto r1csWireCount = getR1CSWireCount(**loweredModule, *mainName);
253 if (!r1csWireCount) {
254 return r1csWireCount.takeError();
255 }
256 // This guards witness length (including synthesized wires). The ordering
257 // contract itself is documented and tested separately for each wire class.
258 if (*r1csWireCount != witness->size()) {
259 return makeError(
260 llvm::Twine("cannot emit .wtns: R1CS lowering produces ") + llvm::Twine(*r1csWireCount) +
261 " wires but llzk-witgen collected " + llvm::Twine(witness->size()) +
262 "; synthesized R1CS auxiliary wires are not yet supported"
263 );
264 }
265
266 auto file = serializeWtns(*witness, field);
267 if (!file) {
268 return file.takeError();
269 }
270
271 return writeBinaryFile(outputFilename, *file);
272}
273
274} // namespace llzk::witgen
This file implements helpers for DynamicAPInt operations and conversions that LLVM does not provide.
then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file
Definition LICENSE.txt:109
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
llvm::Expected< T > checkedCast(U u)
Definition WitgenUtils.h:28
llvm::SmallVector< InputBinding > collectInputBindings(function::FuncDefOp computeFunc)
Collect stable JSON bindings for the main compute inputs.
llvm::Error writeWtns(ModuleOp moduleOp, const llvm::json::Value &fullWitness, const Field &field, StringRef outputFilename)
Definition Wtns.cpp:234
llvm::Error makeError(const llvm::Twine &msg)
Build a string-backed error for user-facing witgen failures.
Definition Errors.h:18
DynamicAPInt toDynamicAPInt(StringRef str)
FailureOr< SymbolLookupResult< StructDefOp > > getMainInstanceDef(SymbolTableCollection &symbolTable, Operation *lookupFrom)
Describe one JSON-visible main input binding.