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 uint32_t fieldSize = ((field.bitWidth() + WTNS_FIELD_LIMB_BITS - 1) / WTNS_FIELD_LIMB_BITS) *
185 WTNS_FIELD_LIMB_BYTES;
186
187 BinaryBuffer header;
188 header.writeU32(fieldSize);
189 header.writeFieldElement(fieldSize, field.prime());
190 header.writeU32(static_cast<uint32_t>(witness.size()));
191
192 BinaryBuffer values;
193 for (const llvm::DynamicAPInt &value : witness) {
194 values.writeFieldElement(fieldSize, value);
195 }
196
197 BinaryBuffer file;
198 file.writeBytes(WTNS_MAGIC);
199 file.writeU32(WTNS_VERSION);
200 file.writeU32(WTNS_SECTION_COUNT);
201 appendSection(file, WTNS_HEADER_SECTION, header);
202 appendSection(file, WTNS_VALUES_SECTION, values);
203 return file;
204}
205
206llvm::Error writeBinaryFile(StringRef outputFilename, const BinaryBuffer &file) {
207 std::unique_ptr<llvm::ToolOutputFile> output = openOutputFile(outputFilename);
208 if (!output) {
209 return makeError(llvm::Twine("failed to open .wtns output: ") + outputFilename);
210 }
211 output->os().write(file.bytes().data(), file.bytes().size());
212 output->os().flush();
213 if (output->os().has_error()) {
214 return makeError(llvm::Twine("failed to write .wtns output: ") + outputFilename);
215 }
216 output->keep();
217 return llvm::Error::success();
218}
219
220} // namespace
221
222llvm::Error writeWtns(
223 ModuleOp moduleOp, const llvm::json::Value &fullWitness, const Field &field,
224 StringRef outputFilename
225) {
226 auto mainName = getMainStructName(moduleOp);
227 if (!mainName) {
228 return mainName.takeError();
229 }
230
231 auto witness = collectWitnessValues(moduleOp, fullWitness, field);
232 if (!witness) {
233 return witness.takeError();
234 }
235
236 auto loweredModule = lowerModuleToR1CS(moduleOp);
237 if (!loweredModule) {
238 return loweredModule.takeError();
239 }
240 auto r1csWireCount = getR1CSWireCount(**loweredModule, *mainName);
241 if (!r1csWireCount) {
242 return r1csWireCount.takeError();
243 }
244 // This guards witness length (including synthesized wires). The ordering
245 // contract itself is documented and tested separately for each wire class.
246 if (*r1csWireCount != witness->size()) {
247 return makeError(
248 llvm::Twine("cannot emit .wtns: R1CS lowering produces ") + llvm::Twine(*r1csWireCount) +
249 " wires but llzk-witgen collected " + llvm::Twine(witness->size()) +
250 "; synthesized R1CS auxiliary wires are not yet supported"
251 );
252 }
253
254 auto file = serializeWtns(*witness, field);
255 if (!file) {
256 return file.takeError();
257 }
258
259 return writeBinaryFile(outputFilename, *file);
260}
261
262} // namespace llzk::witgen
This file implements helper methods for constructing DynamicAPInts.
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::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:222
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.