Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
aztec_process.cpp
Go to the documentation of this file.
1#ifndef __wasm__
2#include "aztec_process.hpp"
11#include <filesystem>
12#include <fstream>
13#include <iomanip>
14#include <nlohmann/json.hpp>
15#include <sstream>
16#include <thread>
17
18#ifndef _WIN32
19#include <fcntl.h>
20#include <sys/file.h>
21#include <unistd.h>
22#endif
23
24#ifdef ENABLE_AVM_TRANSPILER
25// Include avm_transpiler header
26#include <avm_transpiler.h>
27#endif
28
29namespace bb {
30
31namespace {
32
36std::vector<uint8_t> extract_bytecode(const nlohmann::json& function)
37{
38 if (!function.contains("bytecode")) {
39 throw_or_abort("Function missing bytecode field");
40 }
41
42 const auto& base64_bytecode = function["bytecode"].get<std::string>();
43 return decode_bytecode(base64_bytecode);
44}
45
49std::string compute_bytecode_hash(const std::vector<uint8_t>& bytecode)
50{
51 auto hash = crypto::sha256(bytecode);
52 std::ostringstream oss;
53 for (auto byte : hash) {
54 oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
55 }
56 return oss.str();
57}
58
62std::filesystem::path get_cache_dir()
63{
64 const char* home = std::getenv("HOME");
65 if (!home) {
66 home = ".";
67 }
68 std::filesystem::path cache_dir = std::filesystem::path(home) / ".bb" / BB_VERSION / "vk_cache";
69 std::filesystem::create_directories(cache_dir);
70 return cache_dir;
71}
72
76bool is_private_constrained_function(const nlohmann::json& function)
77{
78 bool is_public = false;
79 bool is_unconstrained = false;
80
81 // Check custom_attributes for "public"
82 if (function.contains("custom_attributes") && function["custom_attributes"].is_array()) {
83 for (const auto& attr : function["custom_attributes"]) {
84 if (attr.is_string() && attr.get<std::string>() == "public") {
85 is_public = true;
86 break;
87 }
88 }
89 }
90
91 // Check is_unconstrained
92 if (function.contains("is_unconstrained") && function["is_unconstrained"].is_boolean()) {
93 is_unconstrained = function["is_unconstrained"].get<bool>();
94 }
95
96 return !is_public && !is_unconstrained;
97}
98
108class VkCacheEntryLock {
109 public:
110 explicit VkCacheEntryLock(const std::filesystem::path& entry_path)
111 {
112#ifndef _WIN32
113 std::filesystem::path lock_path = entry_path;
114 lock_path += ".lock";
115 fd = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644);
116 if (fd != -1) {
117 flock(fd, LOCK_EX);
118 }
119#else
120 static_cast<void>(entry_path);
121#endif
122 }
123 ~VkCacheEntryLock()
124 {
125#ifndef _WIN32
126 if (fd != -1) {
127 close(fd); // Releases the flock.
128 }
129#endif
130 }
131 VkCacheEntryLock(const VkCacheEntryLock&) = delete;
132 VkCacheEntryLock& operator=(const VkCacheEntryLock&) = delete;
133 VkCacheEntryLock(VkCacheEntryLock&&) = delete;
134 VkCacheEntryLock& operator=(VkCacheEntryLock&&) = delete;
135
136 private:
137#ifndef _WIN32
138 int fd = -1;
139#endif
140};
141
145std::vector<uint8_t> get_or_generate_cached_app_vk(const std::filesystem::path& cache_dir,
146 const std::string& circuit_name,
147 const std::vector<uint8_t>& bytecode,
148 bool force)
149{
150 std::string hash_str = compute_bytecode_hash(bytecode);
151 std::filesystem::path vk_cache_path = cache_dir / (hash_str + ".vk");
152
153 // Serialise per entry across processes, so at most one process generates a given VK and no
154 // process can observe another's write in progress.
155 VkCacheEntryLock lock(vk_cache_path);
156
157 // Check cache unless force is true
158 if (!force && std::filesystem::exists(vk_cache_path)) {
159 info("Verification key already in cache: ", hash_str);
160 return read_file(vk_cache_path);
161 }
162
163 // Generate new VK (offline / build-time helper, filesystem-cached by bytecode hash).
164 info("Generating verification key: ", hash_str);
165 auto response =
166 bbapi::ChonkComputeVk{ .circuit = { .name = circuit_name, .bytecode = bytecode }, .kind = CircuitKind::App }
167 .execute();
168
169 // Cache the VK via temp file + rename, so a partially written entry is never visible under the
170 // final name even to lockless readers (e.g. platforms where the advisory lock is unavailable).
171 std::filesystem::path tmp_path = vk_cache_path;
172 tmp_path += ".tmp";
173 write_file(tmp_path, response.bytes);
174 std::error_code ec;
175 std::filesystem::rename(tmp_path, vk_cache_path, ec);
176 if (ec) {
177 // Lost a benign race with another process that completed the same entry (only possible
178 // without the advisory lock); its content is equivalent, so keep it and drop ours.
179 std::filesystem::remove(tmp_path, ec);
180 }
181
182 return response.bytes;
183}
184
188void generate_vks_for_functions(const std::filesystem::path& cache_dir,
190 bool force)
191{
192#ifdef __wasm__
193 throw_or_abort("VK generation not supported in WASM");
194#endif
195
196 const size_t total_cpus = get_num_cpus();
197 const size_t num_functions = functions.size();
198
199 // Heuristic for nested parallelism:
200 // - actual_tasks = min(num_functions, total_cpus)
201 // - threads_per_task = min(total_cpus, max(2, total_cpus / actual_tasks * 2))
202 size_t actual_tasks = std::min(num_functions, total_cpus);
203 size_t threads_per_task = std::min(total_cpus, std::max(size_t{ 2 }, total_cpus / actual_tasks * 2));
204
205 // Track work distribution
206 std::atomic<size_t> current_function{ 0 };
207
208 // Worker function
209 auto worker = [&]() {
210 // Set thread-local concurrency for this worker
211 set_parallel_for_concurrency(threads_per_task);
212
213 // Process functions
214 size_t func_idx;
215 while ((func_idx = current_function.fetch_add(1)) < num_functions) {
216 auto* function = functions[func_idx];
217 std::string fn_name = (*function)["name"].get<std::string>();
218
219 // Get bytecode from function
220 auto bytecode = extract_bytecode(*function);
221
222 // Generate and cache VK (can use parallel_for internally)
223 get_or_generate_cached_app_vk(cache_dir, fn_name, bytecode, force);
224 }
225 };
226
227 // Spawn threads
228 std::vector<std::thread> threads;
229 threads.reserve(actual_tasks);
230
231 for (size_t i = 0; i < actual_tasks; ++i) {
232 threads.emplace_back(worker);
233 }
234
235 // Wait for completion
236 for (auto& t : threads) {
237 t.join();
238 }
239
240 // Update JSON with VKs from cache (sequential is fine here, it's fast)
241 for (auto* function : functions) {
242 std::string fn_name = (*function)["name"].get<std::string>();
243
244 // Get bytecode to compute hash
245 auto bytecode = extract_bytecode(*function);
246
247 // Read VK from cache
248 std::string hash_str = compute_bytecode_hash(bytecode);
249 std::filesystem::path vk_cache_path = cache_dir / (hash_str + ".vk");
250 auto vk_data = read_file(vk_cache_path);
251
252 // Encode to base64 and store in JSON
253 std::string encoded_vk = base64_encode(vk_data.data(), vk_data.size(), false);
254 (*function)["verification_key"] = encoded_vk;
255 }
256}
257
258} // anonymous namespace
259
263bool transpile_artifact([[maybe_unused]] const std::string& input_path, [[maybe_unused]] const std::string& output_path)
264{
265#ifdef ENABLE_AVM_TRANSPILER
266 info("Transpiling: ", input_path, " -> ", output_path);
267
268 auto result = avm_transpile_file(input_path.c_str(), output_path.c_str());
269
270 if (result.success == 0) {
271 if (result.error_message) {
272 std::string error_msg(result.error_message);
273 if (error_msg == "Contract already transpiled") {
274 // Already transpiled, copy if different paths
275 if (input_path != output_path) {
276 std::filesystem::copy_file(
277 input_path, output_path, std::filesystem::copy_options::overwrite_existing);
278 }
279 } else {
280 info("Transpilation failed: ", error_msg);
281 avm_free_result(&result);
282 return false;
283 }
284 } else {
285 info("Transpilation failed");
286 avm_free_result(&result);
287 return false;
288 }
289 }
290
291 avm_free_result(&result);
292
293 info("Transpiled: ", input_path, " -> ", output_path);
294#else
295 throw_or_abort("AVM Transpiler is not enabled. Please enable it to use bb aztec_process.");
296#endif
297 return true;
298}
299
300bool process_aztec_artifact(const std::string& input_path, const std::string& output_path, bool force)
301{
302 if (!transpile_artifact(input_path, output_path)) {
303 return false;
304 }
305
306 // Verify output exists
307 if (!std::filesystem::exists(output_path)) {
308 throw_or_abort("Output file does not exist after transpilation");
309 }
310
311 // Step 2: Generate verification keys
312 auto cache_dir = get_cache_dir();
313 info("Generating verification keys for functions in ", std::filesystem::path(output_path).filename().string());
314 info("Cache directory: ", cache_dir.string());
315
316 // Read and parse artifact JSON
317 auto artifact_content = read_file(output_path);
318 std::string artifact_str(artifact_content.begin(), artifact_content.end());
319 auto artifact_json = nlohmann::json::parse(artifact_str);
320
321 if (!artifact_json.contains("functions")) {
322 info("Warning: No functions found in artifact");
323 return true;
324 }
325
326 // Strip __aztec_nr_internals__ prefix from function names.
327 // The #[aztec] macro generates wrapper functions with this prefix; we strip it so
328 // the exported ABI exposes the original developer-written names.
329 const std::string internal_prefix = "__aztec_nr_internals__";
330 for (auto& function : artifact_json["functions"]) {
331 auto& name = function["name"];
332 if (name.is_string()) {
333 std::string fn_name = name.get<std::string>();
334 if (fn_name.size() >= internal_prefix.size() &&
335 fn_name.compare(0, internal_prefix.size(), internal_prefix) == 0) {
336 name = fn_name.substr(internal_prefix.size());
337 }
338 }
339 }
340
341 // Filter to private constrained functions
342 std::vector<nlohmann::json*> private_functions;
343 for (auto& function : artifact_json["functions"]) {
344 if (is_private_constrained_function(function)) {
345 private_functions.push_back(&function);
346 }
347 }
348
349 if (!private_functions.empty()) {
350 // Generate VKs
351 generate_vks_for_functions(cache_dir, private_functions, force);
352 } else {
353 info("No private constrained functions found");
354 }
355
356 // Write updated JSON back to file
357 std::ofstream out_file(output_path);
358 out_file << artifact_json.dump(2) << std::endl;
359 out_file.close();
360
361 info("Successfully processed: ", input_path, " -> ", output_path);
362 return true;
363}
364
365std::vector<std::string> find_contract_artifacts(const std::string& search_path)
366{
367 std::vector<std::string> artifacts;
368
369 // Recursively search for .json files in target/ directories, excluding cache/
370 for (const auto& entry : std::filesystem::recursive_directory_iterator(search_path)) {
371 if (!entry.is_regular_file()) {
372 continue;
373 }
374
375 const auto& path = entry.path();
376
377 // Must be a .json file
378 if (path.extension() != ".json") {
379 continue;
380 }
381
382 // Must be in a target/ directory
383 std::string path_str = path.string();
384 if (path_str.find("/target/") == std::string::npos && path_str.find("\\target\\") == std::string::npos) {
385 continue;
386 }
387
388 // Exclude cache directories and function artifact temporaries
389 if (path_str.find("/cache/") != std::string::npos || path_str.find("\\cache\\") != std::string::npos ||
390 path_str.find(".function_artifact_") != std::string::npos) {
391 continue;
392 }
393
394 artifacts.push_back(path.string());
395 }
396
397 return artifacts;
398}
399
400bool process_all_artifacts(const std::string& search_path, bool force)
401{
402 auto artifacts = find_contract_artifacts(search_path);
403
404 if (artifacts.empty()) {
405 info("No contract artifacts found in '", search_path, "'.");
406 return false;
407 }
408
409 info("Found ", artifacts.size(), " contract artifact(s) to process");
410
411 bool all_success = true;
412 for (const auto& artifact : artifacts) {
413 // Process in-place (input == output)
414 if (!process_aztec_artifact(artifact, artifact, force)) {
415 all_success = false;
416 }
417 }
418
419 if (all_success) {
420 info("Contract postprocessing complete!");
421 }
422
423 return all_success;
424}
425
426bool get_cache_paths(const std::string& input_path)
427{
428 try {
429 // Verify input exists
430 if (!std::filesystem::exists(input_path)) {
431 throw_or_abort("Input file does not exist: " + input_path);
432 }
433
434 // Read and parse artifact JSON
435 auto artifact_content = read_file(input_path);
436 std::string artifact_str(artifact_content.begin(), artifact_content.end());
437 auto artifact_json = nlohmann::json::parse(artifact_str);
438
439 if (!artifact_json.contains("functions")) {
440 // No functions, but not an error
441 return true;
442 }
443
444 // Get cache directory
445 auto cache_dir = get_cache_dir();
446
447 // Find all private constrained functions and output their cache paths
448 for (const auto& function : artifact_json["functions"]) {
449 if (!is_private_constrained_function(function)) {
450 continue;
451 }
452
453 std::string fn_name = function["name"].get<std::string>();
454 auto bytecode = extract_bytecode(function);
455 std::string hash_str = compute_bytecode_hash(bytecode);
456 std::filesystem::path vk_cache_path = cache_dir / (hash_str + ".vk");
457
458 // Output format: hash:cache_path:function_name
459 std::cout << hash_str << ":" << vk_cache_path.string() << ":" << fn_name << std::endl;
460 }
461
462 return true;
463 } catch (const std::exception& e) {
464 info("Error getting cache paths: ", e.what());
465 return false;
466 }
467}
468
469} // namespace bb
470#endif
std::string base64_encode(unsigned char const *bytes_to_encode, size_t in_len, bool url)
Definition base64.cpp:117
Chonk-specific command definitions for the Barretenberg RPC API.
#define info(...)
Definition log.hpp:93
std::vector< uint8_t > bytecode
std::vector< uint8_t > decode_bytecode(const std::string &base64_bytecode)
Sha256Hash sha256(const ByteContainer &input)
SHA-256 hash function (FIPS 180-4)
Definition sha256.cpp:150
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
bool transpile_artifact(const std::string &input_path, const std::string &output_path)
Transpile the artifact file (or copy if transpiler not enabled)
bool process_all_artifacts(const std::string &search_path, bool force)
Process all discovered contract artifacts in a directory tree.
bool get_cache_paths(const std::string &input_path)
Get cache paths for all verification keys in an artifact.
bool process_aztec_artifact(const std::string &input_path, const std::string &output_path, bool force)
Process Aztec contract artifacts: transpile and generate verification keys.
size_t get_num_cpus()
Definition thread.cpp:34
std::vector< std::string > find_contract_artifacts(const std::string &search_path)
Find all contract artifacts in target/ directories.
const char * BB_VERSION
Definition version.hpp:14
std::vector< uint8_t > read_file(const std::string &filename, size_t bytes=0)
Definition file_io.hpp:31
void set_parallel_for_concurrency(size_t num_cores)
Definition thread.cpp:24
void write_file(const std::string &filename, std::span< const uint8_t > data)
Definition file_io.hpp:101
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::string name
void throw_or_abort(std::string const &err)
VectorField result