purify
C++ Purify implementation with native circuit and BPP support
Loading...
Searching...
No Matches
bulletproof.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Judica, Inc.
2// Distributed under the MIT software license, see the accompanying
3// file COPYING or https://opensource.org/license/mit/.
4
12#include "bppp_bridge.h"
13
14#include <algorithm>
15#include <cassert>
16#include <cstdint>
17#include <cstring>
18#include <limits>
19#include <memory>
20#include <new>
21#include <sstream>
22#include <type_traits>
23#include <unordered_map>
24
25namespace purify {
26
32
34 std::unique_ptr<purify_bulletproof_backend_resources, BulletproofBackendResourceDeleter>;
35
37 std::unordered_map<std::size_t, BulletproofBackendResourcePtr> resources;
38};
39
41
43 ExperimentalBulletproofBackendCache&& other) noexcept = default;
44
46 ExperimentalBulletproofBackendCache&& other) noexcept = default;
47
49
51 if (impl_ != nullptr) {
52 impl_->resources.clear();
53 }
54}
55
56std::size_t ExperimentalBulletproofBackendCache::size() const noexcept {
57 return impl_ != nullptr ? impl_->resources.size() : 0;
58}
59
61 std::size_t n_gates) const {
63 if (impl_ == nullptr) {
64 return clone;
65 }
66
67 auto it = impl_->resources.find(n_gates);
68 if (it == impl_->resources.end()) {
69 return clone;
70 }
71
74 if (cloned == nullptr) {
75 return unexpected_error(
77 "ExperimentalBulletproofBackendCache::clone_for_thread:backend_resources");
78 }
79 clone.impl_->resources.emplace(it->first, BulletproofBackendResourcePtr(cloned));
80 return clone;
81}
82
84 std::size_t n_gates,
85 purify_secp_context* secp_context) {
86 if (impl_ == nullptr || secp_context == nullptr) {
87 return nullptr;
88 }
89
90 auto it = impl_->resources.find(n_gates);
91 if (it != impl_->resources.end()) {
92 return it->second.get();
93 }
94
97 if (created == nullptr) {
98 return nullptr;
99 }
100
101 auto inserted = impl_->resources.emplace(n_gates, BulletproofBackendResourcePtr(created));
102 return inserted.first->second.get();
103}
104
105} // namespace purify
106
107namespace {
108
109using purify::Expr;
114using purify::Result;
116using purify::Symbol;
119using purify::Bytes;
127using purify::Status;
133
134static_assert(std::is_trivially_copyable_v<purify::NativeBulletproofCircuitTerm>,
135 "Packed circuit storage requires trivially copyable terms");
136static_assert(std::is_trivially_copyable_v<purify::FieldElement>,
137 "Packed circuit storage requires trivially copyable field elements");
138
139std::uint32_t narrow_symbol_index(std::size_t index) {
140 assert(index <= static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())
141 && "symbol index must fit in uint32_t");
142 return static_cast<std::uint32_t>(index);
143}
144
145struct ResolvedValues {
146 WitnessAssignments witness;
147 std::vector<std::optional<FieldElement>> left;
148 std::vector<std::optional<FieldElement>> right;
149 std::vector<std::optional<FieldElement>> output;
150 std::vector<std::optional<FieldElement>> commitment;
151
152 ResolvedValues(std::size_t witness_count, std::size_t gate_count, std::size_t commitment_count)
153 : witness(witness_count), left(gate_count), right(gate_count), output(gate_count), commitment(commitment_count) {}
154
155 std::optional<FieldElement> get(Symbol symbol) const {
156 std::size_t index = symbol.index;
157 switch (symbol.kind) {
158 case SymbolKind::Witness:
159 if (index >= witness.size()) {
160 return std::nullopt;
161 }
162 return witness[index];
163 case SymbolKind::Left:
164 if (index >= left.size()) {
165 return std::nullopt;
166 }
167 return left[index];
168 case SymbolKind::Right:
169 if (index >= right.size()) {
170 return std::nullopt;
171 }
172 return right[index];
173 case SymbolKind::Output:
174 if (index >= output.size()) {
175 return std::nullopt;
176 }
177 return output[index];
178 case SymbolKind::Commitment:
179 if (index >= commitment.size()) {
180 return std::nullopt;
181 }
182 return commitment[index];
183 }
184 return std::nullopt;
185 }
186
187 bool set(Symbol symbol, const FieldElement& value) {
188 std::size_t index = symbol.index;
189 switch (symbol.kind) {
190 case SymbolKind::Witness:
191 if (index >= witness.size()) {
192 return false;
193 }
194 witness[index] = value;
195 return true;
196 case SymbolKind::Left:
197 if (index >= left.size()) {
198 return false;
199 }
200 left[index] = value;
201 return true;
202 case SymbolKind::Right:
203 if (index >= right.size()) {
204 return false;
205 }
206 right[index] = value;
207 return true;
208 case SymbolKind::Output:
209 if (index >= output.size()) {
210 return false;
211 }
212 output[index] = value;
213 return true;
214 case SymbolKind::Commitment:
215 if (index >= commitment.size()) {
216 return false;
217 }
218 commitment[index] = value;
219 return true;
220 }
221 return false;
222 }
223};
224
225Result<FieldElement> evaluate_known(const Expr& expr, const ResolvedValues& values) {
226 FieldElement out = expr.constant();
227 for (const auto& term : expr.linear()) {
228 std::optional<FieldElement> value = values.get(term.first);
229 if (!value.has_value()) {
230 return purify::unexpected_error(purify::ErrorCode::MissingValue, "BulletproofTranscript::evaluate_known:missing_term");
231 }
232 out = out + *value * term.second;
233 }
234 return out;
235}
236
237bool checked_align_up(std::size_t value, std::size_t alignment, std::size_t& out) {
238 assert(alignment != 0 && "alignment must be non-zero");
239 const std::size_t remainder = value % alignment;
240 if (remainder == 0) {
241 out = value;
242 return true;
243 }
244 return checked_add_size(value, alignment - remainder, out);
245}
246
247bool packed_row_count(std::size_t n_gates, std::size_t n_commitments, std::size_t& out) {
248 std::size_t gate_rows = 0;
249 return checked_mul_size(n_gates, 3, gate_rows) && checked_add_size(gate_rows, n_commitments, out);
250}
251
252void append_u32_le(Bytes& out, std::uint32_t value) {
253 for (int i = 0; i < 4; ++i) {
254 out.push_back(static_cast<unsigned char>((value >> (8 * i)) & 0xffU));
255 }
256}
257
258void append_u64_le(Bytes& out, std::uint64_t value) {
259 for (int i = 0; i < 8; ++i) {
260 out.push_back(static_cast<unsigned char>((value >> (8 * i)) & 0xffU));
261 }
262}
263
264Status append_symbol_digest(Bytes& out, const purify::Symbol& symbol) {
265 using SymbolRank = std::underlying_type_t<purify::SymbolKind>;
266 append_u64_le(out, static_cast<std::uint64_t>(static_cast<SymbolRank>(symbol.kind)));
267 PURIFY_ASSIGN_OR_RETURN(const auto& symbol_index, narrow_size_to_u64(symbol.index, "append_symbol_digest:index"),
268 "append_symbol_digest:index");
269 append_u64_le(out, symbol_index);
270 return {};
271}
272
273Status append_expr_digest(Bytes& out, const purify::Expr& expr) {
274 const auto constant = expr.constant().to_bytes_be();
275 out.insert(out.end(), constant.begin(), constant.end());
276 PURIFY_ASSIGN_OR_RETURN(const auto& term_count, narrow_size_to_u64(expr.linear().size(), "append_expr_digest:term_count"),
277 "append_expr_digest:term_count");
278 append_u64_le(out, term_count);
279 for (const auto& term : expr.linear()) {
280 PURIFY_RETURN_IF_ERROR(append_symbol_digest(out, term.first), "append_expr_digest:symbol");
281 const auto coeff = term.second.to_bytes_be();
282 out.insert(out.end(), coeff.begin(), coeff.end());
283 }
284 return {};
285}
286
287std::optional<std::uint32_t> read_u32_le(std::span<const unsigned char> bytes, std::size_t offset) {
288 std::uint32_t value = 0;
289 if (offset > bytes.size() || bytes.size() - offset < 4) {
290 return std::nullopt;
291 }
292 for (int i = 0; i < 4; ++i) {
293 value |= static_cast<std::uint32_t>(bytes[offset + i]) << (8 * i);
294 }
295 return value;
296}
297
298Bytes flatten_scalars32(const std::vector<FieldElement>& values) {
299 Bytes out;
300 best_effort_reserve_mul(out, values.size(), 32);
301 for (const FieldElement& value : values) {
302 auto bytes = value.to_bytes_be();
303 out.insert(out.end(), bytes.begin(), bytes.end());
304 }
305 return out;
306}
307
308template <typename RowEntriesFn>
309Status append_row_family_digest_generic(Bytes& out, std::size_t row_count, const RowEntriesFn& row_entries,
310 const char* context) {
311 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_row_count, narrow_size_to_u64(row_count, context), context);
312 append_u64_le(out, encoded_row_count);
313 for (std::size_t i = 0; i < row_count; ++i) {
314 std::span<const purify::NativeBulletproofCircuitTerm> entries = row_entries(i);
315 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_entry_count, narrow_size_to_u64(entries.size(), context), context);
316 append_u64_le(out, encoded_entry_count);
317 for (const auto& entry : entries) {
318 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_entry_index, narrow_size_to_u64(entry.idx, context), context);
319 append_u64_le(out, encoded_entry_index);
320 auto scalar = entry.scalar.to_bytes_be();
321 out.insert(out.end(), scalar.begin(), scalar.end());
322 }
323 }
324 return {};
325}
326
327Result<Bytes> circuit_binding_digest(const purify::NativeBulletproofCircuit& circuit,
328 std::span<const unsigned char> statement_binding) {
329 static const purify::TaggedHash kCircuitBindingTag("Purify/ExperimentalBulletproof/CircuitV1");
330 std::size_t reserve_size = 0;
331 if (!checked_mul_size(circuit.c.size(), 32, reserve_size)
332 || !checked_add_size(64, reserve_size, reserve_size)) {
333 return unexpected_error(ErrorCode::Overflow, "circuit_binding_digest:native_reserve");
334 }
335
336 Bytes serialized;
337 serialized.reserve(reserve_size);
338 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_gates, narrow_size_to_u64(circuit.n_gates, "circuit_binding_digest:native_n_gates"),
339 "circuit_binding_digest:native_n_gates");
340 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_commitments,
341 narrow_size_to_u64(circuit.n_commitments, "circuit_binding_digest:native_n_commitments"),
342 "circuit_binding_digest:native_n_commitments");
343 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_bits, narrow_size_to_u64(circuit.n_bits, "circuit_binding_digest:native_n_bits"),
344 "circuit_binding_digest:native_n_bits");
345 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_constant_count,
346 narrow_size_to_u64(circuit.c.size(), "circuit_binding_digest:native_constant_count"),
347 "circuit_binding_digest:native_constant_count");
348 append_u64_le(serialized, encoded_n_gates);
349 append_u64_le(serialized, encoded_n_commitments);
350 append_u64_le(serialized, encoded_n_bits);
351 append_u64_le(serialized, encoded_constant_count);
352 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(
353 serialized, circuit.wl.size(),
354 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wl[i].entries); },
355 "circuit_binding_digest:native_wl"),
356 "circuit_binding_digest:native_wl");
357 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(
358 serialized, circuit.wr.size(),
359 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wr[i].entries); },
360 "circuit_binding_digest:native_wr"),
361 "circuit_binding_digest:native_wr");
362 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(
363 serialized, circuit.wo.size(),
364 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wo[i].entries); },
365 "circuit_binding_digest:native_wo"),
366 "circuit_binding_digest:native_wo");
367 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(
368 serialized, circuit.wv.size(),
369 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wv[i].entries); },
370 "circuit_binding_digest:native_wv"),
371 "circuit_binding_digest:native_wv");
372 for (const FieldElement& constant : circuit.c) {
373 auto bytes = constant.to_bytes_be();
374 serialized.insert(serialized.end(), bytes.begin(), bytes.end());
375 }
376 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_binding_size,
377 narrow_size_to_u64(statement_binding.size(), "circuit_binding_digest:native_statement_binding"),
378 "circuit_binding_digest:native_statement_binding");
379 append_u64_le(serialized, encoded_binding_size);
380 serialized.insert(serialized.end(), statement_binding.begin(), statement_binding.end());
381 std::array<unsigned char, 32> digest =
382 kCircuitBindingTag.digest(std::span<const unsigned char>(serialized.data(), serialized.size()));
383 return Bytes(digest.begin(), digest.end());
384}
385
386Result<Bytes> circuit_binding_digest(const PackedCircuit& circuit, std::span<const unsigned char> statement_binding) {
387 static const purify::TaggedHash kCircuitBindingTag("Purify/ExperimentalBulletproof/CircuitV1");
388 std::size_t reserve_size = 0;
389 if (!checked_mul_size(circuit.constraint_count(), 32, reserve_size)
390 || !checked_add_size(64, reserve_size, reserve_size)) {
391 return unexpected_error(ErrorCode::Overflow, "circuit_binding_digest:packed_reserve");
392 }
393
394 Bytes serialized;
395 serialized.reserve(reserve_size);
396 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_gates, narrow_size_to_u64(circuit.n_gates(), "circuit_binding_digest:packed_n_gates"),
397 "circuit_binding_digest:packed_n_gates");
398 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_commitments,
399 narrow_size_to_u64(circuit.n_commitments(), "circuit_binding_digest:packed_n_commitments"),
400 "circuit_binding_digest:packed_n_commitments");
401 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_n_bits, narrow_size_to_u64(circuit.n_bits(), "circuit_binding_digest:packed_n_bits"),
402 "circuit_binding_digest:packed_n_bits");
403 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_constraint_count,
404 narrow_size_to_u64(circuit.constraint_count(), "circuit_binding_digest:packed_constraint_count"),
405 "circuit_binding_digest:packed_constraint_count");
406 append_u64_le(serialized, encoded_n_gates);
407 append_u64_le(serialized, encoded_n_commitments);
408 append_u64_le(serialized, encoded_n_bits);
409 append_u64_le(serialized, encoded_constraint_count);
410 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(serialized, circuit.n_gates(),
411 [&](std::size_t i) { return circuit.left_row(i).entries_view(); },
412 "circuit_binding_digest:packed_left"),
413 "circuit_binding_digest:packed_left");
414 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(serialized, circuit.n_gates(),
415 [&](std::size_t i) { return circuit.right_row(i).entries_view(); },
416 "circuit_binding_digest:packed_right"),
417 "circuit_binding_digest:packed_right");
418 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(serialized, circuit.n_gates(),
419 [&](std::size_t i) { return circuit.output_row(i).entries_view(); },
420 "circuit_binding_digest:packed_output"),
421 "circuit_binding_digest:packed_output");
422 PURIFY_RETURN_IF_ERROR(append_row_family_digest_generic(serialized, circuit.n_commitments(),
423 [&](std::size_t i) { return circuit.commitment_row(i).entries_view(); },
424 "circuit_binding_digest:packed_commitment"),
425 "circuit_binding_digest:packed_commitment");
426 for (const FieldElement& constant : circuit.constants()) {
427 auto bytes = constant.to_bytes_be();
428 serialized.insert(serialized.end(), bytes.begin(), bytes.end());
429 }
430 PURIFY_ASSIGN_OR_RETURN(const auto& encoded_binding_size,
431 narrow_size_to_u64(statement_binding.size(), "circuit_binding_digest:packed_statement_binding"),
432 "circuit_binding_digest:packed_statement_binding");
433 append_u64_le(serialized, encoded_binding_size);
434 serialized.insert(serialized.end(), statement_binding.begin(), statement_binding.end());
435 std::array<unsigned char, 32> digest =
436 kCircuitBindingTag.digest(std::span<const unsigned char>(serialized.data(), serialized.size()));
437 return Bytes(digest.begin(), digest.end());
438}
439
440template <typename CircuitLike>
441void append_constraint_to_circuit(CircuitLike& circuit, const Expr& lhs, const Expr& rhs) {
442 Expr combined = lhs - rhs;
443 std::size_t constraint_idx = circuit.add_constraint(combined.constant().negate());
444 for (const auto& term : combined.linear()) {
445 std::size_t index = term.first.index;
446 switch (term.first.kind) {
447 case SymbolKind::Left:
448 circuit.add_left_term(index, constraint_idx, term.second);
449 break;
450 case SymbolKind::Right:
451 circuit.add_right_term(index, constraint_idx, term.second);
452 break;
453 case SymbolKind::Output:
454 circuit.add_output_term(index, constraint_idx, term.second);
455 break;
456 case SymbolKind::Commitment:
457 circuit.add_commitment_term(index, constraint_idx, term.second);
458 break;
459 case SymbolKind::Witness:
460 assert(false && "append_constraint_to_circuit() encountered an unmapped witness symbol");
461 break;
462 }
463 }
464}
465
466struct FlattenedRowFamily {
467 std::vector<purify_bulletproof_row_view> views;
468 std::vector<std::size_t> indices;
469 Bytes scalars32;
470};
471
472template <typename RowEntriesFn>
473FlattenedRowFamily flatten_row_family_generic(std::size_t row_count, const RowEntriesFn& row_entries,
474 std::size_t constraint_offset) {
475 FlattenedRowFamily flat;
476 std::vector<std::size_t> offsets;
477 std::vector<std::size_t> counts;
478 offsets.reserve(row_count);
479 counts.reserve(row_count);
480 flat.views.resize(row_count);
481
482 std::size_t total_entries = 0;
483 for (std::size_t i = 0; i < row_count; ++i) {
484 std::span<const purify::NativeBulletproofCircuitTerm> entries = row_entries(i);
485 total_entries += std::count_if(entries.begin(), entries.end(),
486 [&](const auto& entry) { return entry.idx >= constraint_offset; });
487 }
488 flat.indices.reserve(total_entries);
489 best_effort_reserve_mul(flat.scalars32, total_entries, 32);
490
491 for (std::size_t i = 0; i < row_count; ++i) {
492 std::span<const purify::NativeBulletproofCircuitTerm> entries = row_entries(i);
493 std::size_t kept = 0;
494 offsets.push_back(flat.indices.size());
495 for (const auto& entry : entries) {
496 if (entry.idx < constraint_offset) {
497 continue;
498 }
499 ++kept;
500 flat.indices.push_back(entry.idx - constraint_offset);
501 auto bytes = entry.scalar.to_bytes_be();
502 flat.scalars32.insert(flat.scalars32.end(), bytes.begin(), bytes.end());
503 }
504 counts.push_back(kept);
505 }
506
507 const std::size_t* indices_base = flat.indices.empty() ? nullptr : flat.indices.data();
508 const unsigned char* scalars_base = flat.scalars32.empty() ? nullptr : flat.scalars32.data();
509 for (std::size_t i = 0; i < row_count; ++i) {
510 const std::size_t count = counts[i];
511 const std::size_t start = offsets[i];
512 flat.views[i].size = count;
513 flat.views[i].indices = count == 0 ? nullptr : indices_base + start;
514 flat.views[i].scalars32 = count == 0 ? nullptr : scalars_base + start * 32;
515 }
516 return flat;
517}
518
519struct FlattenedCircuitView {
520 FlattenedRowFamily wl;
521 FlattenedRowFamily wr;
522 FlattenedRowFamily wo;
523 FlattenedRowFamily wv;
524 Bytes constants32;
526};
527
528FlattenedCircuitView flatten_circuit_view(const purify::NativeBulletproofCircuit& circuit) {
529 FlattenedCircuitView flat;
530 const std::size_t implicit_bit_constraints = 2 * circuit.n_bits;
531 assert(circuit.c.size() >= implicit_bit_constraints && "native circuit must contain all implicit bit constraints");
532 flat.wl = flatten_row_family_generic(circuit.wl.size(),
533 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wl[i].entries); },
534 implicit_bit_constraints);
535 flat.wr = flatten_row_family_generic(circuit.wr.size(),
536 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wr[i].entries); },
537 implicit_bit_constraints);
538 flat.wo = flatten_row_family_generic(circuit.wo.size(),
539 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wo[i].entries); },
540 implicit_bit_constraints);
541 flat.wv = flatten_row_family_generic(circuit.wv.size(),
542 [&](std::size_t i) { return std::span<const purify::NativeBulletproofCircuitTerm>(circuit.wv[i].entries); },
543 implicit_bit_constraints);
544 std::vector<FieldElement> explicit_constants(circuit.c.begin() + static_cast<std::ptrdiff_t>(implicit_bit_constraints), circuit.c.end());
545 flat.constants32 = flatten_scalars32(explicit_constants);
546 flat.view.n_gates = circuit.n_gates;
547 flat.view.n_commits = circuit.n_commitments;
548 flat.view.n_bits = circuit.n_bits;
549 flat.view.n_constraints = explicit_constants.size();
550 flat.view.wl = flat.wl.views.data();
551 flat.view.wr = flat.wr.views.data();
552 flat.view.wo = flat.wo.views.data();
553 flat.view.wv = flat.wv.views.data();
554 flat.view.c32 = flat.constants32.empty() ? nullptr : flat.constants32.data();
555 return flat;
556}
557
558FlattenedCircuitView flatten_circuit_view(const PackedCircuit& circuit) {
559 FlattenedCircuitView flat;
560 const std::size_t implicit_bit_constraints = 2 * circuit.n_bits();
561 assert(circuit.constraint_count() >= implicit_bit_constraints
562 && "packed circuit must contain all implicit bit constraints");
563 flat.wl = flatten_row_family_generic(circuit.n_gates(),
564 [&](std::size_t i) { return circuit.left_row(i).entries_view(); },
565 implicit_bit_constraints);
566 flat.wr = flatten_row_family_generic(circuit.n_gates(),
567 [&](std::size_t i) { return circuit.right_row(i).entries_view(); },
568 implicit_bit_constraints);
569 flat.wo = flatten_row_family_generic(circuit.n_gates(),
570 [&](std::size_t i) { return circuit.output_row(i).entries_view(); },
571 implicit_bit_constraints);
572 flat.wv = flatten_row_family_generic(circuit.n_commitments(),
573 [&](std::size_t i) { return circuit.commitment_row(i).entries_view(); },
574 implicit_bit_constraints);
575 std::vector<FieldElement> explicit_constants;
576 std::span<const FieldElement> constants = circuit.constants();
577 if (constants.size() > implicit_bit_constraints) {
578 const FieldElement* constants_begin = constants.data();
579 explicit_constants.assign(constants_begin + static_cast<std::ptrdiff_t>(implicit_bit_constraints),
580 constants_begin + static_cast<std::ptrdiff_t>(constants.size()));
581 }
582 flat.constants32 = flatten_scalars32(explicit_constants);
583 flat.view.n_gates = circuit.n_gates();
584 flat.view.n_commits = circuit.n_commitments();
585 flat.view.n_bits = circuit.n_bits();
586 flat.view.n_constraints = explicit_constants.size();
587 flat.view.wl = flat.wl.views.data();
588 flat.view.wr = flat.wr.views.data();
589 flat.view.wo = flat.wo.views.data();
590 flat.view.wv = flat.wv.views.data();
591 flat.view.c32 = flat.constants32.empty() ? nullptr : flat.constants32.data();
592 return flat;
593}
594
595struct FlattenedAssignmentView {
596 Bytes left32;
597 Bytes right32;
598 Bytes output32;
599 Bytes commitments32;
601};
602
603FlattenedAssignmentView flatten_assignment_view(const purify::BulletproofAssignmentData& assignment) {
604 FlattenedAssignmentView flat;
605 flat.left32 = flatten_scalars32(assignment.left);
606 flat.right32 = flatten_scalars32(assignment.right);
607 flat.output32 = flatten_scalars32(assignment.output);
608 flat.commitments32 = flatten_scalars32(assignment.commitments);
609 flat.view.n_gates = assignment.left.size();
610 flat.view.n_commits = assignment.commitments.size();
611 flat.view.al32 = flat.left32.empty() ? nullptr : flat.left32.data();
612 flat.view.ar32 = flat.right32.empty() ? nullptr : flat.right32.data();
613 flat.view.ao32 = flat.output32.empty() ? nullptr : flat.output32.data();
614 flat.view.v32 = flat.commitments32.empty() ? nullptr : flat.commitments32.data();
615 return flat;
616}
617
618std::size_t circuit_n_gates(const purify::NativeBulletproofCircuit& circuit) {
619 return circuit.n_gates;
620}
621
622std::size_t circuit_n_gates(const PackedCircuit& circuit) {
623 return circuit.n_gates();
624}
625
627 return circuit.n_commitments;
628}
629
630std::size_t circuit_n_commitments(const PackedCircuit& circuit) {
631 return circuit.n_commitments();
632}
633
634struct ResolvedBulletproofBackendResources {
635 purify_bulletproof_backend_resources* resources = nullptr;
636};
637
638ResolvedBulletproofBackendResources resolve_bulletproof_backend_resources(
639 std::size_t n_gates,
640 purify_secp_context* secp_context,
642 ResolvedBulletproofBackendResources out;
643
644 if (cache != nullptr) {
645 out.resources = cache->get_or_create(n_gates, secp_context);
646 }
647 return out;
648}
649
650template <typename CircuitLike>
651Result<ExperimentalBulletproofProof> prove_experimental_circuit_impl(
652 const CircuitLike& circuit,
653 const BulletproofAssignmentData& assignment,
654 const BulletproofScalarBytes& nonce,
655 const BulletproofGeneratorBytes& value_generator,
656 purify_secp_context* secp_context,
657 std::span<const unsigned char> statement_binding,
658 std::optional<BulletproofScalarBytes> blind,
659 ExperimentalBulletproofBackendCache* backend_cache,
660 bool require_assignment_validation,
661 const char* shape_context,
662 const char* gates_context,
663 const char* commitments_context,
664 const char* assignment_shape_context,
665 const char* assignment_invalid_context,
666 const char* proof_size_context,
667 const char* bridge_context) {
668 PURIFY_RETURN_IF_ERROR(require_secp_context(secp_context, "prove_experimental_circuit:secp_context"),
669 "prove_experimental_circuit:secp_context");
670 ExperimentalBulletproofProof out;
671 if (!circuit.has_valid_shape()) {
672 return unexpected_error(ErrorCode::InvalidDimensions, shape_context);
673 }
674 if (!is_power_of_two_size(circuit_n_gates(circuit))) {
675 return unexpected_error(ErrorCode::InvalidDimensions, gates_context);
676 }
677 if (circuit_n_commitments(circuit) != 1) {
678 return unexpected_error(ErrorCode::InvalidDimensions, commitments_context);
679 }
680 if (assignment.left.size() != circuit_n_gates(circuit)
681 || assignment.right.size() != circuit_n_gates(circuit)
682 || assignment.output.size() != circuit_n_gates(circuit)
683 || assignment.commitments.size() != circuit_n_commitments(circuit)) {
684 return unexpected_error(ErrorCode::SizeMismatch, assignment_shape_context);
685 }
686 if (require_assignment_validation && !circuit.evaluate(assignment)) {
687 return unexpected_error(ErrorCode::EquationMismatch, assignment_invalid_context);
688 }
689
690 FlattenedCircuitView flat_circuit = flatten_circuit_view(circuit);
691 FlattenedAssignmentView flat_assignment = flatten_assignment_view(assignment);
692 PURIFY_ASSIGN_OR_RETURN(const auto& binding_digest, circuit_binding_digest(circuit, statement_binding),
693 "prove_experimental_circuit_impl:binding_digest");
694 Bytes proof_bytes(std::max<std::size_t>(purify_bulletproof_required_proof_size(circuit_n_gates(circuit)), 4096), 0);
695 std::size_t proof_len = proof_bytes.size();
696 BulletproofPointBytes commitment{};
697 const unsigned char* blind_ptr = blind.has_value() ? blind->data() : nullptr;
698 ResolvedBulletproofBackendResources resolved =
699 resolve_bulletproof_backend_resources(circuit_n_gates(circuit), secp_context, backend_cache);
700 purify_bulletproof_backend_resources* resources = resolved.resources;
701
702 if (proof_len == 0) {
703 return unexpected_error(ErrorCode::UnexpectedSize, proof_size_context);
704 }
705 const int ok =
706 resources != nullptr
707 ? (require_assignment_validation
709 resources, &flat_circuit.view, &flat_assignment.view, blind_ptr,
710 value_generator.data(), nonce.data(),
711 binding_digest.data(), binding_digest.size(),
712 commitment.data(), proof_bytes.data(), &proof_len)
714 resources, &flat_circuit.view, &flat_assignment.view, blind_ptr,
715 value_generator.data(), nonce.data(),
716 binding_digest.data(), binding_digest.size(),
717 commitment.data(), proof_bytes.data(), &proof_len))
718 : (require_assignment_validation
720 secp_context, &flat_circuit.view, &flat_assignment.view, blind_ptr,
721 value_generator.data(), nonce.data(),
722 binding_digest.data(), binding_digest.size(),
723 commitment.data(), proof_bytes.data(), &proof_len)
725 secp_context, &flat_circuit.view, &flat_assignment.view, blind_ptr,
726 value_generator.data(), nonce.data(),
727 binding_digest.data(), binding_digest.size(),
728 commitment.data(), proof_bytes.data(), &proof_len));
729 if (!ok) {
730 return unexpected_error(ErrorCode::BackendRejectedInput, bridge_context);
731 }
732
733 proof_bytes.resize(proof_len);
734 out.commitment = commitment;
735 out.proof = std::move(proof_bytes);
736 return out;
737}
738
739Result<FieldElement> evaluate_expr_with_assignment(const Expr& expr,
740 const purify::BulletproofAssignmentData& assignment) {
741 FieldElement out = expr.constant();
742 for (const auto& term : expr.linear()) {
743 std::size_t index = term.first.index;
744 switch (term.first.kind) {
745 case SymbolKind::Left:
746 if (index >= assignment.left.size()) {
748 "evaluate_expr_with_assignment:left_index");
749 }
750 out = out + (assignment.left[index] * term.second);
751 break;
752 case SymbolKind::Right:
753 if (index >= assignment.right.size()) {
755 "evaluate_expr_with_assignment:right_index");
756 }
757 out = out + (assignment.right[index] * term.second);
758 break;
759 case SymbolKind::Output:
760 if (index >= assignment.output.size()) {
762 "evaluate_expr_with_assignment:output_index");
763 }
764 out = out + (assignment.output[index] * term.second);
765 break;
766 case SymbolKind::Commitment:
767 if (index >= assignment.commitments.size()) {
769 "evaluate_expr_with_assignment:commitment_index");
770 }
771 out = out + (assignment.commitments[index] * term.second);
772 break;
773 case SymbolKind::Witness:
775 "evaluate_expr_with_assignment:witness_symbol");
776 }
777 }
778 return out;
779}
780
781} // namespace
782
783namespace purify {
784
786 if (left.size() != right.size() || left.size() != output.size()) {
787 return unexpected_error(ErrorCode::SizeMismatch, "BulletproofAssignmentData::serialize:column_sizes");
788 }
789 if (!size_fits_u32(commitments.size())) {
790 return unexpected_error(ErrorCode::UnexpectedSize, "BulletproofAssignmentData::serialize:commitment_count");
791 }
792 if (!size_fits_u64(left.size())) {
793 return unexpected_error(ErrorCode::UnexpectedSize, "BulletproofAssignmentData::serialize:row_count");
794 }
795
796 std::size_t scalar_count = 0;
797 std::size_t serialized_size = 0;
798 if (!checked_mul_size(left.size(), 3, scalar_count)
799 || !checked_add_size(scalar_count, commitments.size(), scalar_count)
800 || !checked_mul_size(scalar_count, 33, serialized_size)
801 || !checked_add_size(16, serialized_size, serialized_size)) {
802 return unexpected_error(ErrorCode::Overflow, "BulletproofAssignmentData::serialize:reserve");
803 }
804
805 Bytes out;
806 out.reserve(serialized_size);
807 auto append_u32_le = [&](std::uint32_t value) {
808 for (int i = 0; i < 4; ++i) {
809 out.push_back(static_cast<unsigned char>((value >> (8 * i)) & 0xffU));
810 }
811 };
812 auto append_u64_le = [&](std::uint64_t value) {
813 for (int i = 0; i < 8; ++i) {
814 out.push_back(static_cast<unsigned char>((value >> (8 * i)) & 0xffU));
815 }
816 };
817 auto write_column = [&](const std::vector<FieldElement>& column) {
818 for (const FieldElement& value : column) {
819 out.push_back(static_cast<unsigned char>(0x20));
820 std::array<unsigned char, 32> bytes = value.to_bytes_le();
821 out.insert(out.end(), bytes.begin(), bytes.end());
822 }
823 };
824
825 append_u32_le(1);
826 append_u32_le(static_cast<std::uint32_t>(commitments.size()));
827 append_u64_le(static_cast<std::uint64_t>(left.size()));
828 write_column(left);
829 write_column(right);
830 write_column(output);
831 write_column(commitments);
832 return out;
833}
834
836 if (scalar.is_zero()) {
837 return;
838 }
839 entries.push_back({idx, scalar});
840}
841
842NativeBulletproofCircuit::NativeBulletproofCircuit(std::size_t gates, std::size_t commitments, std::size_t bits)
843 : n_gates(gates), n_commitments(commitments), n_bits(bits), wl(gates), wr(gates), wo(gates), wv(commitments) {}
844
845void NativeBulletproofCircuit::resize(std::size_t gates, std::size_t commitments, std::size_t bits) {
846 n_gates = gates;
847 n_commitments = commitments;
848 n_bits = bits;
849 wl.assign(gates, {});
850 wr.assign(gates, {});
851 wo.assign(gates, {});
852 wv.assign(commitments, {});
853 c.clear();
854}
855
857 return wl.size() == n_gates
858 && wr.size() == n_gates
859 && wo.size() == n_gates
860 && wv.size() == n_commitments;
861}
862
864 c.push_back(constant);
865 return c.size() - 1;
866}
867
868void NativeBulletproofCircuit::add_left_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement& scalar) {
869 add_row_term(wl, n_gates, gate_idx, constraint_idx, scalar);
870}
871
872void NativeBulletproofCircuit::add_right_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement& scalar) {
873 add_row_term(wr, n_gates, gate_idx, constraint_idx, scalar);
874}
875
876void NativeBulletproofCircuit::add_output_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement& scalar) {
877 add_row_term(wo, n_gates, gate_idx, constraint_idx, scalar);
878}
879
880void NativeBulletproofCircuit::add_commitment_term(std::size_t commitment_idx, std::size_t constraint_idx, const FieldElement& scalar) {
881 add_row_term(wv, n_commitments, commitment_idx, constraint_idx, scalar.negate());
882}
883
885 if (!has_valid_shape()) {
886 return false;
887 }
888 if (assignment.left.size() != n_gates || assignment.right.size() != n_gates || assignment.output.size() != n_gates) {
889 return false;
890 }
891 if (assignment.commitments.size() != n_commitments) {
892 return false;
893 }
894
895 for (std::size_t i = 0; i < n_gates; ++i) {
896 if (assignment.left[i] * assignment.right[i] != assignment.output[i]) {
897 return false;
898 }
899 }
900
901 std::vector<FieldElement> acc(c.size(), FieldElement::zero());
902 auto accumulate = [&](const std::vector<NativeBulletproofCircuitRow>& rows,
903 const std::vector<FieldElement>& values,
904 bool negate_values = false) {
905 if (rows.size() != values.size()) {
906 return false;
907 }
908 for (std::size_t i = 0; i < rows.size(); ++i) {
909 if (negate_values) {
910 FieldElement negated = values[i].negate();
911 for (const NativeBulletproofCircuitTerm& entry : rows[i].entries) {
912 if (entry.idx >= acc.size()) {
913 return false;
914 }
915 acc[entry.idx] = acc[entry.idx] + entry.scalar * negated;
916 }
917 continue;
918 }
919 for (const NativeBulletproofCircuitTerm& entry : rows[i].entries) {
920 if (entry.idx >= acc.size()) {
921 return false;
922 }
923 acc[entry.idx] = acc[entry.idx] + entry.scalar * values[i];
924 }
925 }
926 return true;
927 };
928 if (!accumulate(wl, assignment.left) || !accumulate(wr, assignment.right) || !accumulate(wo, assignment.output)) {
929 return false;
930 }
931 if (!accumulate(wv, assignment.commitments, true)) {
932 return false;
933 }
934
935 for (std::size_t i = 0; i < c.size(); ++i) {
936 if (acc[i] != c[i]) {
937 return false;
938 }
939 }
940 return true;
941}
942
943void NativeBulletproofCircuit::add_row_term(std::vector<NativeBulletproofCircuitRow>& rows, std::size_t expected_size,
944 std::size_t row_idx, std::size_t constraint_idx, const FieldElement& scalar) {
945 (void)expected_size;
946 assert(rows.size() == expected_size && "NativeBulletproofCircuit rows must be initialized before adding terms");
947 assert(row_idx < rows.size() && "NativeBulletproofCircuit row index out of range");
948 rows[row_idx].add(constraint_idx, scalar);
949}
950
951void NativeBulletproofCircuit::PackedWithSlack::PackedStorageDeleter::operator()(std::byte* storage) const noexcept {
952 if (storage != nullptr) {
953 ::operator delete(storage, std::align_val_t(NativeBulletproofCircuit::PackedWithSlack::kPackedStorageAlignment));
954 }
955}
956
957std::byte* NativeBulletproofCircuit::PackedWithSlack::allocate_storage(std::size_t bytes) {
958 if (bytes == 0) {
959 return nullptr;
960 }
961 return static_cast<std::byte*>(::operator new(bytes, std::align_val_t(kPackedStorageAlignment)));
962}
963
965 : n_gates_(other.n_gates_), n_commitments_(other.n_commitments_), n_bits_(other.n_bits_),
966 constraint_size_(other.constraint_size_), constraint_base_size_(other.constraint_base_size_),
967 constraint_capacity_(other.constraint_capacity_), term_capacity_(other.term_capacity_),
968 term_bytes_offset_(other.term_bytes_offset_), constant_bytes_offset_(other.constant_bytes_offset_),
969 storage_bytes_(other.storage_bytes_) {
970 assert((other.storage_ != nullptr) == (other.storage_bytes_ != 0)
971 && "PackedWithSlack source storage must match its byte count");
972 if (storage_bytes_ != 0) {
973 storage_.reset(allocate_storage(storage_bytes_));
974 start_storage_lifetimes();
975 std::memcpy(storage_.get(), other.storage_.get(), storage_bytes_);
976 }
977}
978
980 : n_gates_(other.n_gates_), n_commitments_(other.n_commitments_), n_bits_(other.n_bits_),
981 constraint_size_(other.constraint_size_), constraint_base_size_(other.constraint_base_size_),
982 constraint_capacity_(other.constraint_capacity_), term_capacity_(other.term_capacity_),
983 term_bytes_offset_(other.term_bytes_offset_), constant_bytes_offset_(other.constant_bytes_offset_),
984 storage_bytes_(other.storage_bytes_), storage_(std::move(other.storage_)) {
985 other.reset_to_empty();
986}
987
990 if (this == &other) {
991 return *this;
992 }
993 PackedWithSlack copy(other);
994 *this = std::move(copy);
995 return *this;
996}
997
1000 if (this == &other) {
1001 return *this;
1002 }
1003 n_gates_ = other.n_gates_;
1004 n_commitments_ = other.n_commitments_;
1005 n_bits_ = other.n_bits_;
1006 constraint_size_ = other.constraint_size_;
1007 constraint_base_size_ = other.constraint_base_size_;
1008 constraint_capacity_ = other.constraint_capacity_;
1009 term_capacity_ = other.term_capacity_;
1010 term_bytes_offset_ = other.term_bytes_offset_;
1011 constant_bytes_offset_ = other.constant_bytes_offset_;
1012 storage_bytes_ = other.storage_bytes_;
1013 storage_ = std::move(other.storage_);
1014 other.reset_to_empty();
1015 return *this;
1016}
1017
1018bool NativeBulletproofCircuit::PackedWithSlack::compute_storage_layout(std::size_t row_count, std::size_t term_capacity,
1019 std::size_t constraint_capacity,
1020 std::size_t& term_bytes_offset,
1021 std::size_t& constant_bytes_offset,
1022 std::size_t& storage_bytes) noexcept {
1023 std::size_t row_headers_bytes = 0;
1024 if (!checked_mul_size(row_count, sizeof(PackedRowHeader), row_headers_bytes)
1025 || !checked_align_up(row_headers_bytes, alignof(NativeBulletproofCircuitTerm), term_bytes_offset)) {
1026 return false;
1027 }
1028 std::size_t terms_bytes = 0;
1029 if (!checked_mul_size(term_capacity, sizeof(NativeBulletproofCircuitTerm), terms_bytes)) {
1030 return false;
1031 }
1032 std::size_t term_region_end = 0;
1033 if (!checked_add_size(term_bytes_offset, terms_bytes, term_region_end)
1034 || !checked_align_up(term_region_end, alignof(FieldElement), constant_bytes_offset)) {
1035 return false;
1036 }
1037 std::size_t constants_bytes = 0;
1038 if (!checked_mul_size(constraint_capacity, sizeof(FieldElement), constants_bytes)) {
1039 return false;
1040 }
1041 return checked_add_size(constant_bytes_offset, constants_bytes, storage_bytes);
1042}
1043
1044void NativeBulletproofCircuit::PackedWithSlack::reset_to_empty() noexcept {
1045 storage_.reset();
1046 n_gates_ = 0;
1047 n_commitments_ = 0;
1048 n_bits_ = 0;
1049 constraint_size_ = 0;
1050 constraint_base_size_ = 0;
1051 constraint_capacity_ = 0;
1052 term_capacity_ = 0;
1053 term_bytes_offset_ = 0;
1054 constant_bytes_offset_ = 0;
1055 storage_bytes_ = 0;
1056}
1057
1059 if (constraint_base_size_ > constraint_size_ || constraint_size_ > constraint_capacity_) {
1060 return false;
1061 }
1062 std::size_t row_count = 0;
1063 if (!packed_row_count(n_gates_, n_commitments_, row_count)) {
1064 return false;
1065 }
1066 std::size_t expected_term_offset = 0;
1067 std::size_t expected_constant_offset = 0;
1068 std::size_t expected_storage_bytes = 0;
1069 if (!compute_storage_layout(row_count, term_capacity_, constraint_capacity_,
1070 expected_term_offset, expected_constant_offset, expected_storage_bytes)) {
1071 return false;
1072 }
1073 if (term_bytes_offset_ != expected_term_offset || constant_bytes_offset_ != expected_constant_offset
1074 || storage_bytes_ != expected_storage_bytes) {
1075 return false;
1076 }
1077 if ((storage_ == nullptr) != (storage_bytes_ == 0)) {
1078 return false;
1079 }
1080 if (storage_bytes_ == 0) {
1081 return row_count == 0 && term_capacity_ == 0 && constraint_capacity_ == 0;
1082 }
1083
1084 const PackedRowHeader* headers =
1085 std::launder(reinterpret_cast<const PackedRowHeader*>(raw_storage_bytes()));
1086 std::size_t term_cursor = 0;
1087 for (std::size_t i = 0; i < row_count; ++i) {
1088 const PackedRowHeader& header = headers[i];
1089 if (header.base_size > header.size || header.size > header.capacity) {
1090 return false;
1091 }
1092 if (header.offset != term_cursor) {
1093 return false;
1094 }
1095 if (term_cursor > term_capacity_ || header.capacity > term_capacity_ - term_cursor) {
1096 return false;
1097 }
1098 term_cursor += header.capacity;
1099 }
1100 return term_cursor == term_capacity_;
1101}
1102
1104 constraint_size_ = constraint_base_size_;
1105 std::size_t row_count = 0;
1106 [[maybe_unused]] const bool ok = packed_row_count(n_gates_, n_commitments_, row_count);
1107 assert(ok && "PackedWithSlack row count should fit in size_t");
1108 PackedRowHeader* headers = row_count == 0 ? nullptr
1109 : std::launder(reinterpret_cast<PackedRowHeader*>(raw_storage_bytes()));
1110 for (std::size_t i = 0; i < row_count; ++i) {
1111 PackedRowHeader& header = headers[i];
1112 header.size = header.base_size;
1113 }
1114}
1115
1116NativeBulletproofCircuit::PackedWithSlack::PackedRowHeader&
1117NativeBulletproofCircuit::PackedWithSlack::row_header(RowFamily family, std::size_t idx) noexcept {
1118 PackedRowHeader* headers = std::launder(reinterpret_cast<PackedRowHeader*>(raw_storage_bytes()));
1119 std::size_t base = 0;
1120 switch (family) {
1121 case RowFamily::Left:
1122 base = 0;
1123 break;
1124 case RowFamily::Right:
1125 base = n_gates_;
1126 break;
1127 case RowFamily::Output:
1128 base = 2 * n_gates_;
1129 break;
1130 case RowFamily::Commitment:
1131 base = 3 * n_gates_;
1132 break;
1133 }
1134 return headers[base + idx];
1135}
1136
1137const NativeBulletproofCircuit::PackedWithSlack::PackedRowHeader&
1138NativeBulletproofCircuit::PackedWithSlack::row_header(RowFamily family, std::size_t idx) const noexcept {
1139 const PackedRowHeader* headers = std::launder(reinterpret_cast<const PackedRowHeader*>(raw_storage_bytes()));
1140 std::size_t base = 0;
1141 switch (family) {
1142 case RowFamily::Left:
1143 base = 0;
1144 break;
1145 case RowFamily::Right:
1146 base = n_gates_;
1147 break;
1148 case RowFamily::Output:
1149 base = 2 * n_gates_;
1150 break;
1151 case RowFamily::Commitment:
1152 base = 3 * n_gates_;
1153 break;
1154 }
1155 return headers[base + idx];
1156}
1157
1158NativeBulletproofCircuitTerm* NativeBulletproofCircuit::PackedWithSlack::term_data() noexcept {
1159 if (term_capacity_ == 0) {
1160 return nullptr;
1161 }
1162 return std::launder(reinterpret_cast<NativeBulletproofCircuitTerm*>(raw_storage_bytes() + term_bytes_offset_));
1163}
1164
1165const NativeBulletproofCircuitTerm* NativeBulletproofCircuit::PackedWithSlack::term_data() const noexcept {
1166 if (term_capacity_ == 0) {
1167 return nullptr;
1168 }
1169 return std::launder(reinterpret_cast<const NativeBulletproofCircuitTerm*>(raw_storage_bytes() + term_bytes_offset_));
1170}
1171
1172FieldElement* NativeBulletproofCircuit::PackedWithSlack::constant_data() noexcept {
1173 if (constraint_capacity_ == 0) {
1174 return nullptr;
1175 }
1176 return std::launder(reinterpret_cast<FieldElement*>(raw_storage_bytes() + constant_bytes_offset_));
1177}
1178
1179const FieldElement* NativeBulletproofCircuit::PackedWithSlack::constant_data() const noexcept {
1180 if (constraint_capacity_ == 0) {
1181 return nullptr;
1182 }
1183 return std::launder(reinterpret_cast<const FieldElement*>(raw_storage_bytes() + constant_bytes_offset_));
1184}
1185
1186unsigned char* NativeBulletproofCircuit::PackedWithSlack::raw_storage_bytes() noexcept {
1187 return reinterpret_cast<unsigned char*>(storage_.get());
1188}
1189
1190const unsigned char* NativeBulletproofCircuit::PackedWithSlack::raw_storage_bytes() const noexcept {
1191 return reinterpret_cast<const unsigned char*>(storage_.get());
1192}
1193
1194void NativeBulletproofCircuit::PackedWithSlack::start_storage_lifetimes() noexcept {
1195 if (storage_ == nullptr) {
1196 return;
1197 }
1198 std::size_t row_count = 0;
1199 [[maybe_unused]] const bool ok = packed_row_count(n_gates_, n_commitments_, row_count);
1200 assert(ok && "PackedWithSlack row count should fit in size_t");
1201 if (row_count != 0) {
1202 PackedRowHeader* headers = reinterpret_cast<PackedRowHeader*>(raw_storage_bytes());
1203 std::uninitialized_value_construct_n(headers, row_count);
1204 }
1205 if (term_capacity_ != 0) {
1206 NativeBulletproofCircuitTerm* terms =
1207 reinterpret_cast<NativeBulletproofCircuitTerm*>(raw_storage_bytes() + term_bytes_offset_);
1208 std::uninitialized_value_construct_n(terms, term_capacity_);
1209 }
1210 if (constraint_capacity_ != 0) {
1211 FieldElement* constants = reinterpret_cast<FieldElement*>(raw_storage_bytes() + constant_bytes_offset_);
1212 std::uninitialized_value_construct_n(constants, constraint_capacity_);
1213 }
1214}
1215
1216NativeBulletproofCircuitRow::PackedWithSlack
1217NativeBulletproofCircuit::PackedWithSlack::row_view(const PackedRowHeader& header) const noexcept {
1218 const NativeBulletproofCircuitTerm* terms = term_data();
1219 return {header.capacity == 0 ? nullptr : terms + header.offset, header.size, header.capacity};
1220}
1221
1223 assert(constraint_size_ < constraint_capacity_ && "PackedWithSlack constraint capacity exceeded");
1224 FieldElement* constants = constant_data();
1225 constants[constraint_size_] = constant;
1226 ++constraint_size_;
1227 return constraint_size_ - 1;
1228}
1229
1230void NativeBulletproofCircuit::PackedWithSlack::add_row_term(RowFamily family, std::size_t expected_size,
1231 std::size_t row_idx, std::size_t constraint_idx,
1232 const FieldElement& scalar) {
1233 (void)expected_size;
1234 if (scalar.is_zero()) {
1235 return;
1236 }
1237 assert(constraint_idx < constraint_size_ && "PackedWithSlack constraint index out of range");
1238 assert(((family == RowFamily::Commitment) ? n_commitments_ : n_gates_) == expected_size
1239 && "PackedWithSlack expected row family size mismatch");
1240 assert(row_idx < expected_size && "PackedWithSlack row index out of range");
1241 PackedRowHeader& header = row_header(family, row_idx);
1242 assert(header.size < header.capacity && "PackedWithSlack row capacity exceeded");
1243 term_data()[header.offset + header.size] = {constraint_idx, scalar};
1244 ++header.size;
1245}
1246
1247void NativeBulletproofCircuit::PackedWithSlack::add_left_term(std::size_t gate_idx, std::size_t constraint_idx,
1248 const FieldElement& scalar) {
1249 add_row_term(RowFamily::Left, n_gates_, gate_idx, constraint_idx, scalar);
1250}
1251
1252void NativeBulletproofCircuit::PackedWithSlack::add_right_term(std::size_t gate_idx, std::size_t constraint_idx,
1253 const FieldElement& scalar) {
1254 add_row_term(RowFamily::Right, n_gates_, gate_idx, constraint_idx, scalar);
1255}
1256
1257void NativeBulletproofCircuit::PackedWithSlack::add_output_term(std::size_t gate_idx, std::size_t constraint_idx,
1258 const FieldElement& scalar) {
1259 add_row_term(RowFamily::Output, n_gates_, gate_idx, constraint_idx, scalar);
1260}
1261
1262void NativeBulletproofCircuit::PackedWithSlack::add_commitment_term(std::size_t commitment_idx, std::size_t constraint_idx,
1263 const FieldElement& scalar) {
1264 add_row_term(RowFamily::Commitment, n_commitments_, commitment_idx, constraint_idx, scalar.negate());
1265}
1266
1268NativeBulletproofCircuit::PackedWithSlack::left_row(std::size_t gate_idx) const noexcept {
1269 return row_view(row_header(RowFamily::Left, gate_idx));
1270}
1271
1273NativeBulletproofCircuit::PackedWithSlack::right_row(std::size_t gate_idx) const noexcept {
1274 return row_view(row_header(RowFamily::Right, gate_idx));
1275}
1276
1278NativeBulletproofCircuit::PackedWithSlack::output_row(std::size_t gate_idx) const noexcept {
1279 return row_view(row_header(RowFamily::Output, gate_idx));
1280}
1281
1283NativeBulletproofCircuit::PackedWithSlack::commitment_row(std::size_t commitment_idx) const noexcept {
1284 return row_view(row_header(RowFamily::Commitment, commitment_idx));
1285}
1286
1287std::span<const FieldElement> NativeBulletproofCircuit::PackedWithSlack::constants() const noexcept {
1288 return std::span<const FieldElement>(constant_data(), constraint_size_);
1289}
1290
1292 if (!has_valid_shape()) {
1293 return false;
1294 }
1295 if (assignment.left.size() != n_gates_ || assignment.right.size() != n_gates_ || assignment.output.size() != n_gates_) {
1296 return false;
1297 }
1298 if (assignment.commitments.size() != n_commitments_) {
1299 return false;
1300 }
1301 for (std::size_t i = 0; i < n_gates_; ++i) {
1302 if (assignment.left[i] * assignment.right[i] != assignment.output[i]) {
1303 return false;
1304 }
1305 }
1306
1307 std::vector<FieldElement> acc(constraint_size_, FieldElement::zero());
1308 const NativeBulletproofCircuitTerm* all_terms = term_data();
1309 auto accumulate = [&](RowFamily family, std::size_t row_count, const std::vector<FieldElement>& values,
1310 bool negate_values = false) {
1311 if (values.size() != row_count) {
1312 return false;
1313 }
1314 for (std::size_t i = 0; i < row_count; ++i) {
1315 const PackedRowHeader& header = row_header(family, i);
1316 FieldElement factor = negate_values ? values[i].negate() : values[i];
1317 if (header.size == 0) {
1318 continue;
1319 }
1320 const NativeBulletproofCircuitTerm* row_terms = all_terms + header.offset;
1321 for (std::size_t j = 0; j < header.size; ++j) {
1322 const NativeBulletproofCircuitTerm& entry = row_terms[j];
1323 if (entry.idx >= acc.size()) {
1324 return false;
1325 }
1326 acc[entry.idx] = acc[entry.idx] + (entry.scalar * factor);
1327 }
1328 }
1329 return true;
1330 };
1331 if (!accumulate(RowFamily::Left, n_gates_, assignment.left)
1332 || !accumulate(RowFamily::Right, n_gates_, assignment.right)
1333 || !accumulate(RowFamily::Output, n_gates_, assignment.output)
1334 || !accumulate(RowFamily::Commitment, n_commitments_, assignment.commitments, true)) {
1335 return false;
1336 }
1337 for (std::size_t i = 0; i < constraint_size_; ++i) {
1338 if (acc[i] != constant_data()[i]) {
1339 return false;
1340 }
1341 }
1342 return true;
1343}
1344
1346 if (!has_valid_shape()) {
1347 return unexpected_error(ErrorCode::InvalidDimensions, "NativeBulletproofCircuit::PackedWithSlack::unpack:shape");
1348 }
1349 NativeBulletproofCircuit out(n_gates_, n_commitments_, n_bits_);
1350 if (constraint_size_ != 0) {
1351 const FieldElement* constants = constant_data();
1352 out.c.assign(constants, constants + static_cast<std::ptrdiff_t>(constraint_size_));
1353 }
1354 const NativeBulletproofCircuitTerm* all_terms = term_data();
1355 auto unpack_family = [&](RowFamily family, std::vector<NativeBulletproofCircuitRow>& rows, std::size_t row_count) {
1356 for (std::size_t i = 0; i < row_count; ++i) {
1357 const PackedRowHeader& header = row_header(family, i);
1358 if (header.size == 0) {
1359 rows[i].entries.clear();
1360 continue;
1361 }
1362 rows[i].entries.assign(all_terms + header.offset,
1363 all_terms + header.offset + static_cast<std::ptrdiff_t>(header.size));
1364 }
1365 };
1366 unpack_family(RowFamily::Left, out.wl, n_gates_);
1367 unpack_family(RowFamily::Right, out.wr, n_gates_);
1368 unpack_family(RowFamily::Output, out.wo, n_gates_);
1369 unpack_family(RowFamily::Commitment, out.wv, n_commitments_);
1370 return out;
1371}
1372
1374 const NativeBulletproofCircuit& circuit,
1375 const PackedSlackPlan& slack) {
1376 if (!circuit.has_valid_shape()) {
1377 return unexpected_error(ErrorCode::InvalidDimensions, "NativeBulletproofCircuit::PackedWithSlack::from_circuit:shape");
1378 }
1379 auto validate_family_slack = [](const std::vector<std::size_t>& family_slack, std::size_t expected,
1380 const char* context) -> Status {
1381 if (!family_slack.empty() && family_slack.size() != expected) {
1383 }
1384 return {};
1385 };
1386 Status wl_status = validate_family_slack(slack.wl, circuit.n_gates,
1387 "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wl");
1388 if (!wl_status.has_value()) {
1389 return unexpected_error(wl_status.error(), "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wl");
1390 }
1391 Status wr_status = validate_family_slack(slack.wr, circuit.n_gates,
1392 "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wr");
1393 if (!wr_status.has_value()) {
1394 return unexpected_error(wr_status.error(), "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wr");
1395 }
1396 Status wo_status = validate_family_slack(slack.wo, circuit.n_gates,
1397 "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wo");
1398 if (!wo_status.has_value()) {
1399 return unexpected_error(wo_status.error(), "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wo");
1400 }
1401 Status wv_status = validate_family_slack(slack.wv, circuit.n_commitments,
1402 "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wv");
1403 if (!wv_status.has_value()) {
1404 return unexpected_error(wv_status.error(), "NativeBulletproofCircuit::PackedWithSlack::from_circuit:wv");
1405 }
1406
1407 PackedWithSlack packed;
1408 packed.n_gates_ = circuit.n_gates;
1409 packed.n_commitments_ = circuit.n_commitments;
1410 packed.n_bits_ = circuit.n_bits;
1411 packed.constraint_size_ = circuit.c.size();
1412 packed.constraint_base_size_ = circuit.c.size();
1413 if (!checked_add_size(circuit.c.size(), slack.constraint_slack, packed.constraint_capacity_)) {
1414 return unexpected_error(ErrorCode::Overflow, "NativeBulletproofCircuit::PackedWithSlack::from_circuit:constraint_capacity");
1415 }
1416
1417 std::size_t row_count = 0;
1418 if (!packed_row_count(circuit.n_gates, circuit.n_commitments, row_count)) {
1419 return unexpected_error(ErrorCode::Overflow, "NativeBulletproofCircuit::PackedWithSlack::from_circuit:row_count");
1420 }
1421 std::size_t total_term_capacity = 0;
1422 auto sum_capacity = [&](const std::vector<NativeBulletproofCircuitRow>& rows,
1423 const std::vector<std::size_t>& family_slack) -> bool {
1424 for (std::size_t i = 0; i < rows.size(); ++i) {
1425 const std::size_t extra = family_slack.empty() ? 0 : family_slack[i];
1426 std::size_t row_capacity = 0;
1427 if (!checked_add_size(rows[i].entries.size(), extra, row_capacity)
1428 || !checked_add_size(total_term_capacity, row_capacity, total_term_capacity)) {
1429 return false;
1430 }
1431 }
1432 return true;
1433 };
1434 if (!sum_capacity(circuit.wl, slack.wl)
1435 || !sum_capacity(circuit.wr, slack.wr)
1436 || !sum_capacity(circuit.wo, slack.wo)
1437 || !sum_capacity(circuit.wv, slack.wv)) {
1438 return unexpected_error(ErrorCode::Overflow, "NativeBulletproofCircuit::PackedWithSlack::from_circuit:term_capacity");
1439 }
1440 packed.term_capacity_ = total_term_capacity;
1441 if (!compute_storage_layout(row_count, packed.term_capacity_, packed.constraint_capacity_,
1442 packed.term_bytes_offset_, packed.constant_bytes_offset_, packed.storage_bytes_)) {
1443 return unexpected_error(ErrorCode::Overflow, "NativeBulletproofCircuit::PackedWithSlack::from_circuit:storage_layout");
1444 }
1445 packed.storage_.reset(allocate_storage(packed.storage_bytes_));
1446 packed.start_storage_lifetimes();
1447
1448 PackedWithSlack::PackedRowHeader* headers =
1449 row_count == 0 ? nullptr : std::launder(reinterpret_cast<PackedWithSlack::PackedRowHeader*>(packed.raw_storage_bytes()));
1450 FieldElement* constants = packed.constant_data();
1451 if (packed.constraint_capacity_ != 0) {
1452 std::fill_n(constants, packed.constraint_capacity_, FieldElement::zero());
1453 std::copy(circuit.c.begin(), circuit.c.end(), constants);
1454 }
1455
1456 std::size_t row_cursor = 0;
1457 std::size_t term_cursor = 0;
1458 auto fill_family = [&](const std::vector<NativeBulletproofCircuitRow>& rows,
1459 const std::vector<std::size_t>& family_slack) {
1460 for (std::size_t i = 0; i < rows.size(); ++i, ++row_cursor) {
1461 const std::size_t extra = family_slack.empty() ? 0 : family_slack[i];
1462 PackedWithSlack::PackedRowHeader& header = headers[row_cursor];
1463 header.offset = term_cursor;
1464 header.size = rows[i].entries.size();
1465 header.base_size = rows[i].entries.size();
1466 header.capacity = rows[i].entries.size() + extra;
1467 if (!rows[i].entries.empty()) {
1468 std::copy(rows[i].entries.begin(), rows[i].entries.end(),
1469 packed.term_data() + static_cast<std::ptrdiff_t>(term_cursor));
1470 }
1471 term_cursor += header.capacity;
1472 }
1473 };
1474 fill_family(circuit.wl, slack.wl);
1475 fill_family(circuit.wr, slack.wr);
1476 fill_family(circuit.wo, slack.wo);
1477 fill_family(circuit.wv, slack.wv);
1478 assert(term_cursor == packed.term_capacity_ && "packed term cursor should consume the entire term buffer");
1479
1480 return packed;
1481}
1482
1486
1490
1492 if (proof.size() > static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) {
1493 return unexpected_error(ErrorCode::UnexpectedSize, "ExperimentalBulletproofProof::serialize:proof_too_large");
1494 }
1495 std::size_t serialized_size = 0;
1496 if (!checked_add_size(38, proof.size(), serialized_size)) {
1497 return unexpected_error(ErrorCode::Overflow, "ExperimentalBulletproofProof::serialize:reserve");
1498 }
1499
1500 Bytes out;
1501 out.reserve(serialized_size);
1502 out.push_back(kSerializationVersion);
1503 append_u32_le(out, static_cast<std::uint32_t>(proof.size()));
1504 out.insert(out.end(), commitment.begin(), commitment.end());
1505 out.insert(out.end(), proof.begin(), proof.end());
1506 return out;
1507}
1508
1511 if (bytes.size() < 38) {
1512 return unexpected_error(ErrorCode::InvalidFixedSize, "ExperimentalBulletproofProof::deserialize:header");
1513 }
1514 if (bytes[0] != kSerializationVersion) {
1515 return unexpected_error(ErrorCode::BackendRejectedInput, "ExperimentalBulletproofProof::deserialize:version");
1516 }
1517
1518 std::optional<std::uint32_t> proof_size = read_u32_le(bytes, 1);
1519 assert(proof_size.has_value() && "header length check should guarantee a proof length");
1520 std::size_t offset = 5;
1521 if (bytes.size() - offset < 33) {
1522 return unexpected_error(ErrorCode::InvalidFixedSize, "ExperimentalBulletproofProof::deserialize:commitment");
1523 }
1524 std::copy_n(bytes.begin() + static_cast<std::ptrdiff_t>(offset), 33, out.commitment.begin());
1525 offset += 33;
1526 if (*proof_size != bytes.size() - offset) {
1527 return unexpected_error(ErrorCode::InvalidFixedSize, "ExperimentalBulletproofProof::deserialize:proof_length");
1528 }
1529 out.proof.assign(bytes.begin() + static_cast<std::ptrdiff_t>(offset), bytes.end());
1530 return out;
1531}
1532
1534 const NativeBulletproofCircuit& circuit,
1535 const BulletproofAssignmentData& assignment,
1537 const BulletproofGeneratorBytes& value_generator,
1538 purify_secp_context* secp_context,
1539 std::span<const unsigned char> statement_binding,
1540 std::optional<BulletproofScalarBytes> blind,
1541 ExperimentalBulletproofBackendCache* backend_cache) {
1542 return prove_experimental_circuit_impl(circuit, assignment, nonce, value_generator, secp_context, statement_binding,
1543 blind, backend_cache, true,
1544 "prove_experimental_circuit:circuit_shape",
1545 "prove_experimental_circuit:n_gates_power_of_two",
1546 "prove_experimental_circuit:n_commitments",
1547 "prove_experimental_circuit:assignment_shape",
1548 "prove_experimental_circuit:assignment_invalid",
1549 "prove_experimental_circuit:proof_size",
1550 "prove_experimental_circuit:bridge");
1551}
1552
1555 const BulletproofAssignmentData& assignment,
1557 const BulletproofGeneratorBytes& value_generator,
1558 purify_secp_context* secp_context,
1559 std::span<const unsigned char> statement_binding,
1560 std::optional<BulletproofScalarBytes> blind,
1561 ExperimentalBulletproofBackendCache* backend_cache) {
1562 return prove_experimental_circuit_impl(circuit, assignment, nonce, value_generator, secp_context, statement_binding,
1563 blind, backend_cache, true,
1564 "prove_experimental_circuit:packed_circuit_shape",
1565 "prove_experimental_circuit:packed_n_gates_power_of_two",
1566 "prove_experimental_circuit:packed_n_commitments",
1567 "prove_experimental_circuit:packed_assignment_shape",
1568 "prove_experimental_circuit:packed_assignment_invalid",
1569 "prove_experimental_circuit:packed_proof_size",
1570 "prove_experimental_circuit:packed_bridge");
1571}
1572
1575 const BulletproofAssignmentData& assignment,
1577 const BulletproofGeneratorBytes& value_generator,
1578 purify_secp_context* secp_context,
1579 std::span<const unsigned char> statement_binding,
1580 std::optional<BulletproofScalarBytes> blind,
1581 ExperimentalBulletproofBackendCache* backend_cache) {
1582 return prove_experimental_circuit_impl(circuit, assignment, nonce, value_generator, secp_context, statement_binding,
1583 blind, backend_cache, false,
1584 "prove_experimental_circuit_assume_valid:packed_circuit_shape",
1585 "prove_experimental_circuit_assume_valid:packed_n_gates_power_of_two",
1586 "prove_experimental_circuit_assume_valid:packed_n_commitments",
1587 "prove_experimental_circuit_assume_valid:packed_assignment_shape",
1588 "prove_experimental_circuit_assume_valid:packed_assignment_invalid",
1589 "prove_experimental_circuit_assume_valid:packed_proof_size",
1590 "prove_experimental_circuit_assume_valid:packed_bridge");
1591}
1592
1594 const NativeBulletproofCircuit& circuit,
1595 const ExperimentalBulletproofProof& proof,
1596 const BulletproofGeneratorBytes& value_generator,
1597 purify_secp_context* secp_context,
1598 std::span<const unsigned char> statement_binding,
1599 ExperimentalBulletproofBackendCache* backend_cache) {
1600 PURIFY_RETURN_IF_ERROR(require_secp_context(secp_context, "verify_experimental_circuit:secp_context"),
1601 "verify_experimental_circuit:secp_context");
1602 if (!circuit.has_valid_shape()) {
1603 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:circuit_shape");
1604 }
1605 if (!is_power_of_two_size(circuit.n_gates)) {
1606 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:n_gates_power_of_two");
1607 }
1608 if (circuit.n_commitments != 1) {
1609 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:n_commitments");
1610 }
1611 if (proof.proof.empty()) {
1612 return unexpected_error(ErrorCode::EmptyInput, "verify_experimental_circuit:proof_empty");
1613 }
1614
1615 FlattenedCircuitView flat_circuit = flatten_circuit_view(circuit);
1616 PURIFY_ASSIGN_OR_RETURN(const auto& binding_digest, circuit_binding_digest(circuit, statement_binding),
1617 "verify_experimental_circuit:binding_digest");
1618 ResolvedBulletproofBackendResources resolved =
1619 resolve_bulletproof_backend_resources(circuit.n_gates, secp_context, backend_cache);
1620 purify_bulletproof_backend_resources* resources = resolved.resources;
1621 bool ok =
1622 resources != nullptr
1623 ? purify_bulletproof_verify_circuit_with_resources(resources, &flat_circuit.view, proof.commitment.data(),
1624 value_generator.data(), binding_digest.data(),
1625 binding_digest.size(), proof.proof.data(),
1626 proof.proof.size()) != 0
1627 : purify_bulletproof_verify_circuit(secp_context, &flat_circuit.view, proof.commitment.data(),
1628 value_generator.data(), binding_digest.data(),
1629 binding_digest.size(), proof.proof.data(),
1630 proof.proof.size()) != 0;
1631 return ok;
1632}
1633
1636 const ExperimentalBulletproofProof& proof,
1637 const BulletproofGeneratorBytes& value_generator,
1638 purify_secp_context* secp_context,
1639 std::span<const unsigned char> statement_binding,
1640 ExperimentalBulletproofBackendCache* backend_cache) {
1641 PURIFY_RETURN_IF_ERROR(require_secp_context(secp_context, "verify_experimental_circuit:packed_secp_context"),
1642 "verify_experimental_circuit:packed_secp_context");
1643 if (!circuit.has_valid_shape()) {
1644 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:packed_circuit_shape");
1645 }
1646 if (!is_power_of_two_size(circuit.n_gates())) {
1647 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:packed_n_gates_power_of_two");
1648 }
1649 if (circuit.n_commitments() != 1) {
1650 return unexpected_error(ErrorCode::InvalidDimensions, "verify_experimental_circuit:packed_n_commitments");
1651 }
1652 if (proof.proof.empty()) {
1653 return unexpected_error(ErrorCode::EmptyInput, "verify_experimental_circuit:packed_proof_empty");
1654 }
1655
1656 FlattenedCircuitView flat_circuit = flatten_circuit_view(circuit);
1657 PURIFY_ASSIGN_OR_RETURN(const auto& binding_digest, circuit_binding_digest(circuit, statement_binding),
1658 "verify_experimental_circuit:packed_binding_digest");
1659 ResolvedBulletproofBackendResources resolved =
1660 resolve_bulletproof_backend_resources(circuit.n_gates(), secp_context, backend_cache);
1661 purify_bulletproof_backend_resources* resources = resolved.resources;
1662 bool ok =
1663 resources != nullptr
1664 ? purify_bulletproof_verify_circuit_with_resources(resources, &flat_circuit.view, proof.commitment.data(),
1665 value_generator.data(), binding_digest.data(),
1666 binding_digest.size(), proof.proof.data(),
1667 proof.proof.size()) != 0
1668 : purify_bulletproof_verify_circuit(secp_context, &flat_circuit.view, proof.commitment.data(),
1669 value_generator.data(), binding_digest.data(),
1670 binding_digest.size(), proof.proof.data(),
1671 proof.proof.size()) != 0;
1672 return ok;
1673}
1674
1676 for (auto& term : expr.linear()) {
1677 if (term.first.kind == SymbolKind::Witness
1678 && term.first.index < witness_to_a_.size()
1679 && witness_to_a_[term.first.index].has_value()) {
1680 term.first = *witness_to_a_[term.first.index];
1681 }
1682 }
1683}
1684
1686 if (!expr.linear().empty()) {
1687 replace_expr_v_with_bp_var(expr);
1688 if (expr.constant().is_zero()
1689 && expr.linear().size() == 1
1690 && expr.linear()[0].second == FieldElement::one()
1691 && expr.linear()[0].first.kind == SymbolKind::Witness) {
1692 std::uint32_t witness_index = expr.linear()[0].first.index;
1693 if (witness_index < witness_to_a_.size() && !witness_to_a_[witness_index].has_value()) {
1694 witness_to_a_[witness_index] = symbol;
1695 witness_to_a_order_.push_back({witness_index, symbol});
1696 return true;
1697 }
1698 }
1699 }
1700 return false;
1701}
1702
1704 bool is_v = replace_and_insert(expr, symbol);
1705 assignments_.push_back({symbol, std::move(expr), is_v});
1706}
1707
1709 assignments_.clear();
1710 constraints_.clear();
1711 witness_to_a_.assign(transcript.varmap().size(), std::nullopt);
1712 witness_to_a_order_.clear();
1713 n_witnesses_ = transcript.varmap().size();
1714 n_bits_ = n_bits;
1715 std::size_t source_muls = transcript.muls().size();
1716 for (std::size_t i = 0; i < source_muls; ++i) {
1717 const auto& mul = transcript.muls()[i];
1718 add_assignment(Symbol::left(narrow_symbol_index(i)), mul.lhs);
1719 add_assignment(Symbol::right(narrow_symbol_index(i)), mul.rhs);
1720 add_assignment(Symbol::output(narrow_symbol_index(i)), mul.out);
1721 }
1722
1723 std::vector<std::uint32_t> unmapped_transcript_vars;
1724 std::vector<bool> seen_transcript_vars(n_witnesses_, false);
1725 for (const Expr& eq : transcript.eqs()) {
1726 for (const auto& term : eq.linear()) {
1727 if (!is_transcript_var(term.first)) {
1728 continue;
1729 }
1730 std::uint32_t witness_index = term.first.index;
1731 if (witness_index >= witness_to_a_.size() || witness_to_a_[witness_index].has_value()) {
1732 continue;
1733 }
1734 if (!seen_transcript_vars[witness_index]) {
1735 unmapped_transcript_vars.push_back(witness_index);
1736 seen_transcript_vars[witness_index] = true;
1737 }
1738 }
1739 }
1740
1741 std::size_t total_gates = source_muls + unmapped_transcript_vars.size();
1742 n_muls_ = 1;
1743 while (n_muls_ < std::max<std::size_t>(1, total_gates)) {
1744 n_muls_ <<= 1;
1745 }
1746
1747 for (std::size_t i = 0; i < unmapped_transcript_vars.size(); ++i) {
1748 std::size_t gate_idx = source_muls + i;
1749 std::uint32_t gate_symbol = narrow_symbol_index(gate_idx);
1750 add_assignment(Symbol::left(gate_symbol), Expr::variable(Symbol::witness(unmapped_transcript_vars[i])));
1751 add_assignment(Symbol::right(gate_symbol), Expr(0));
1752 add_assignment(Symbol::output(gate_symbol), Expr(0));
1753 }
1754
1755 for (std::size_t i = total_gates; i < n_muls_; ++i) {
1756 std::uint32_t gate_symbol = narrow_symbol_index(i);
1757 add_assignment(Symbol::left(gate_symbol), Expr(0));
1758 add_assignment(Symbol::right(gate_symbol), Expr(0));
1759 add_assignment(Symbol::output(gate_symbol), Expr(0));
1760 }
1761
1762 for (const Expr& eq : transcript.eqs()) {
1763 Expr lowered = eq;
1764 replace_expr_v_with_bp_var(lowered);
1765 if (contains_transcript_var(lowered)) {
1766 return unexpected_error(ErrorCode::UnsupportedSymbol, "BulletproofTranscript::from_transcript:unmapped_eq_var");
1767 }
1768 constraints_.push_back({lowered, Expr(0)});
1769 }
1770 return {};
1771}
1772
1775 if (!unpacked.has_value()) {
1776 return unexpected_error(unpacked.error(), "BulletproofTranscript::add_pubkey_and_out:unpack_public");
1777 }
1778 auto add_constraint = [&](const UInt256& packed, Expr expr) -> Status {
1779 replace_expr_v_with_bp_var(expr);
1780 auto parts = expr.split();
1782 if (!constant.has_value()) {
1783 return unexpected_error(constant.error(), "BulletproofTranscript::add_pubkey_and_out:field_constant");
1784 }
1785 constraints_.push_back({parts.second, Expr(*constant) - parts.first});
1786 return {};
1787 };
1788 Status p1_status = add_constraint(unpacked->first, std::move(p1x));
1789 if (!p1_status.has_value()) {
1790 return unexpected_error(p1_status.error(), "BulletproofTranscript::add_pubkey_and_out:p1x");
1791 }
1792 Status p2_status = add_constraint(unpacked->second, std::move(p2x));
1793 if (!p2_status.has_value()) {
1794 return unexpected_error(p2_status.error(), "BulletproofTranscript::add_pubkey_and_out:p2x");
1795 }
1796 replace_expr_v_with_bp_var(out);
1797 constraints_.push_back({out - Expr::variable(Symbol::commitment(0)), Expr(0)});
1798 return {};
1799}
1800
1802 std::size_t n_constraints = 0;
1803 for (const auto& assignment : assignments_) {
1804 if (!assignment.is_v) {
1805 ++n_constraints;
1806 }
1807 }
1808 n_constraints += constraints_.size();
1809 std::ostringstream out;
1810 out << n_muls_ << "," << n_commitments_ << "," << n_bits_ << "," << (n_constraints - 2 * n_bits_) << ";";
1811 std::size_t i = 0;
1812 for (const auto& assignment : assignments_) {
1813 if (!assignment.is_v) {
1814 if (i < 2 * n_bits_) {
1815 ++i;
1816 continue;
1817 }
1818 auto parts = assignment.expr.split();
1819 out << assignment.symbol.to_string();
1820 if (!parts.second.linear().empty()) {
1821 out << " + " << (-parts.second).to_string();
1822 }
1823 out << " = " << parts.first.to_string() << ";";
1824 }
1825 }
1826 for (const auto& constraint : constraints_) {
1827 out << constraint.first.to_string() << " = " << constraint.second.to_string() << ";";
1828 }
1829 return out.str();
1830}
1831
1832bool BulletproofTranscript::evaluate(const WitnessAssignments& vars, const FieldElement& commitment) const {
1833 ResolvedValues values(n_witnesses_, n_muls_, n_commitments_);
1834 for (std::size_t i = 0; i < std::min(vars.size(), values.witness.size()); ++i) {
1835 values.witness[i] = vars[i];
1836 }
1837 if (!values.set(Symbol::commitment(0), commitment)) {
1838 return false;
1839 }
1840 for (const auto& item : witness_to_a_order_) {
1841 if (item.first >= values.witness.size() || !values.witness[item.first].has_value()) {
1842 return false;
1843 }
1844 if (!values.set(item.second, *values.witness[item.first])) {
1845 return false;
1846 }
1847 }
1848 for (const auto& assignment : assignments_) {
1849 Result<FieldElement> evaluated = ::evaluate_known(assignment.expr, values);
1850 if (!evaluated.has_value()) {
1851 return false;
1852 }
1853 if (!values.set(assignment.symbol, *evaluated)) {
1854 return false;
1855 }
1856 }
1857 for (std::size_t i = 0; i < n_muls_; ++i) {
1858 if (!values.left[i].has_value() || !values.right[i].has_value() || !values.output[i].has_value()) {
1859 return false;
1860 }
1861 if (*values.left[i] * *values.right[i] != *values.output[i]) {
1862 return false;
1863 }
1864 }
1865 for (const auto& constraint : constraints_) {
1866 Result<FieldElement> lhs = ::evaluate_known(constraint.first, values);
1867 Result<FieldElement> rhs = ::evaluate_known(constraint.second, values);
1868 if (!lhs.has_value() || !rhs.has_value() || *lhs != *rhs) {
1869 return false;
1870 }
1871 }
1872 return true;
1873}
1874
1877 circuit.n_gates = n_muls_;
1878 circuit.n_commitments = n_commitments_;
1879 circuit.n_bits = n_bits_;
1880 circuit.wl.resize(n_muls_);
1881 circuit.wr.resize(n_muls_);
1882 circuit.wo.resize(n_muls_);
1883 circuit.wv.resize(n_commitments_);
1884 circuit.c.reserve(
1885 std::count_if(assignments_.begin(), assignments_.end(),
1886 [](const auto& assignment) { return !assignment.is_v; })
1887 + constraints_.size());
1888
1889 for (const auto& assignment : assignments_) {
1890 if (!assignment.is_v) {
1891 append_constraint_to_circuit(circuit, Expr::variable(assignment.symbol), assignment.expr);
1892 }
1893 }
1894 for (const auto& constraint : constraints_) {
1895 append_constraint_to_circuit(circuit, constraint.first, constraint.second);
1896 }
1897 return circuit;
1898}
1899
1902 Expr p1x,
1903 Expr p2x,
1904 Expr out) {
1905 NativeBulletproofCircuitTemplate template_circuit;
1906 template_circuit.base_packed_ = std::move(base_packed);
1907 template_circuit.p1x_ = std::move(p1x);
1908 template_circuit.p2x_ = std::move(p2x);
1909 template_circuit.out_ = std::move(out);
1910 return template_circuit;
1911}
1912
1914 if (!base_packed_.has_valid_shape()) {
1915 return unexpected_error(ErrorCode::InvalidDimensions, "NativeBulletproofCircuitTemplate::partial_evaluate:shape");
1916 }
1917 if (assignment.left.size() != base_packed_.n_gates()
1918 || assignment.right.size() != base_packed_.n_gates()
1919 || assignment.output.size() != base_packed_.n_gates()
1920 || assignment.commitments.size() != base_packed_.n_commitments()) {
1921 return unexpected_error(ErrorCode::SizeMismatch, "NativeBulletproofCircuitTemplate::partial_evaluate:assignment_shape");
1922 }
1923 return base_packed_.evaluate(assignment);
1924}
1925
1927 const UInt512& pubkey) const {
1928 if (!base_packed_.has_valid_shape()) {
1929 return unexpected_error(ErrorCode::InvalidDimensions, "NativeBulletproofCircuitTemplate::final_evaluate:shape");
1930 }
1931 if (assignment.left.size() != base_packed_.n_gates()
1932 || assignment.right.size() != base_packed_.n_gates()
1933 || assignment.output.size() != base_packed_.n_gates()
1934 || assignment.commitments.size() != base_packed_.n_commitments()) {
1935 return unexpected_error(ErrorCode::SizeMismatch, "NativeBulletproofCircuitTemplate::final_evaluate:assignment_shape");
1936 }
1938 if (!unpacked.has_value()) {
1939 return unexpected_error(unpacked.error(), "NativeBulletproofCircuitTemplate::final_evaluate:unpack_public");
1940 }
1941 Result<FieldElement> expected_p1 = FieldElement::try_from_uint256(unpacked->first);
1942 if (!expected_p1.has_value()) {
1943 return unexpected_error(expected_p1.error(), "NativeBulletproofCircuitTemplate::final_evaluate:expected_p1");
1944 }
1945 Result<FieldElement> expected_p2 = FieldElement::try_from_uint256(unpacked->second);
1946 if (!expected_p2.has_value()) {
1947 return unexpected_error(expected_p2.error(), "NativeBulletproofCircuitTemplate::final_evaluate:expected_p2");
1948 }
1949 Result<FieldElement> actual_p1 = evaluate_expr_with_assignment(p1x_, assignment);
1950 if (!actual_p1.has_value()) {
1951 return unexpected_error(actual_p1.error(), "NativeBulletproofCircuitTemplate::final_evaluate:actual_p1");
1952 }
1953 Result<FieldElement> actual_p2 = evaluate_expr_with_assignment(p2x_, assignment);
1954 if (!actual_p2.has_value()) {
1955 return unexpected_error(actual_p2.error(), "NativeBulletproofCircuitTemplate::final_evaluate:actual_p2");
1956 }
1957 Result<FieldElement> actual_out = evaluate_expr_with_assignment(out_, assignment);
1958 if (!actual_out.has_value()) {
1959 return unexpected_error(actual_out.error(), "NativeBulletproofCircuitTemplate::final_evaluate:actual_out");
1960 }
1961 return *actual_p1 == *expected_p1
1962 && *actual_p2 == *expected_p2
1963 && *actual_out == assignment.commitments[0];
1964}
1965
1967 if (!base_packed_.has_valid_shape()) {
1968 return unexpected_error(ErrorCode::InvalidDimensions, "NativeBulletproofCircuitTemplate::integrity_digest:shape");
1969 }
1970
1971 static const TaggedHash kTemplateDigestTag("Purify/VerifierCircuitTemplate/V1");
1972 PURIFY_ASSIGN_OR_RETURN(auto serialized, circuit_binding_digest(base_packed_, {}),
1973 "NativeBulletproofCircuitTemplate::integrity_digest:circuit_binding_digest");
1974 PURIFY_RETURN_IF_ERROR(append_expr_digest(serialized, p1x_),
1975 "NativeBulletproofCircuitTemplate::integrity_digest:p1x");
1976 PURIFY_RETURN_IF_ERROR(append_expr_digest(serialized, p2x_),
1977 "NativeBulletproofCircuitTemplate::integrity_digest:p2x");
1978 PURIFY_RETURN_IF_ERROR(append_expr_digest(serialized, out_),
1979 "NativeBulletproofCircuitTemplate::integrity_digest:out");
1980 const std::array<unsigned char, 32> digest =
1981 kTemplateDigestTag.digest(std::span<const unsigned char>(serialized.data(), serialized.size()));
1982 return Bytes(digest.begin(), digest.end());
1983}
1984
1987 if (!unpacked.has_value()) {
1988 return unexpected_error(unpacked.error(), "NativeBulletproofCircuitTemplate::instantiate_packed:unpack_public");
1989 }
1990
1991 NativeBulletproofCircuit::PackedWithSlack circuit = base_packed_;
1992 circuit.reset();
1993 auto append_pubkey_constraint = [&](const UInt256& packed, const Expr& expr) -> Status {
1994 auto parts = expr.split();
1996 if (!constant.has_value()) {
1997 return unexpected_error(constant.error(), "NativeBulletproofCircuitTemplate::instantiate_packed:field_constant");
1998 }
1999 append_constraint_to_circuit(circuit, parts.second, Expr(*constant) - parts.first);
2000 return {};
2001 };
2002
2003 Status p1_status = append_pubkey_constraint(unpacked->first, p1x_);
2004 if (!p1_status.has_value()) {
2005 return unexpected_error(p1_status.error(), "NativeBulletproofCircuitTemplate::instantiate_packed:p1x");
2006 }
2007 Status p2_status = append_pubkey_constraint(unpacked->second, p2x_);
2008 if (!p2_status.has_value()) {
2009 return unexpected_error(p2_status.error(), "NativeBulletproofCircuitTemplate::instantiate_packed:p2x");
2010 }
2011 append_constraint_to_circuit(circuit, out_, Expr::variable(Symbol::commitment(0)));
2012 return circuit;
2013}
2014
2016 Result<NativeBulletproofCircuit::PackedWithSlack> packed = instantiate_packed(pubkey);
2017 if (!packed.has_value()) {
2018 return unexpected_error(packed.error(), "NativeBulletproofCircuitTemplate::instantiate:instantiate_packed");
2019 }
2020 return packed->unpack();
2021}
2022
2024 return assignment_data_impl(vars, nullptr);
2025}
2026
2028 const FieldElement& commitment) const {
2029 return assignment_data_impl(vars, &commitment);
2030}
2031
2032Result<BulletproofAssignmentData> BulletproofTranscript::assignment_data_impl(const WitnessAssignments& vars,
2033 const FieldElement* commitment) const {
2034 ResolvedValues values(n_witnesses_, n_muls_, n_commitments_);
2035 for (std::size_t i = 0; i < std::min(vars.size(), values.witness.size()); ++i) {
2036 values.witness[i] = vars[i];
2037 }
2038 if (commitment != nullptr && !values.set(Symbol::commitment(0), *commitment)) {
2039 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:commitment_slot");
2040 }
2041 for (const auto& item : witness_to_a_order_) {
2042 if (item.first >= values.witness.size() || !values.witness[item.first].has_value()) {
2043 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:mapped_value");
2044 }
2045 if (!values.set(item.second, *values.witness[item.first])) {
2046 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:mapped_symbol");
2047 }
2048 }
2049 for (const auto& assignment : assignments_) {
2050 Result<FieldElement> evaluated = ::evaluate_known(assignment.expr, values);
2051 if (!evaluated.has_value()) {
2052 return unexpected_error(evaluated.error(), "BulletproofTranscript::assignment_data:evaluate_assignment");
2053 }
2054 if (!values.set(assignment.symbol, *evaluated)) {
2055 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:store_assignment");
2056 }
2057 }
2058
2059 BulletproofAssignmentData assignment;
2060 assignment.left.reserve(n_muls_);
2061 assignment.right.reserve(n_muls_);
2062 assignment.output.reserve(n_muls_);
2063 assignment.commitments.reserve(n_commitments_);
2064 auto read_column = [&](const std::vector<std::optional<FieldElement>>& source,
2065 std::vector<FieldElement>& column) -> Status {
2066 for (const auto& value : source) {
2067 if (!value.has_value()) {
2068 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:column_value");
2069 }
2070 column.push_back(*value);
2071 }
2072 return {};
2073 };
2074 Status left_status = read_column(values.left, assignment.left);
2075 if (!left_status.has_value()) {
2076 return unexpected_error(left_status.error(), "BulletproofTranscript::assignment_data:left_column");
2077 }
2078 Status right_status = read_column(values.right, assignment.right);
2079 if (!right_status.has_value()) {
2080 return unexpected_error(right_status.error(), "BulletproofTranscript::assignment_data:right_column");
2081 }
2082 Status output_status = read_column(values.output, assignment.output);
2083 if (!output_status.has_value()) {
2084 return unexpected_error(output_status.error(), "BulletproofTranscript::assignment_data:output_column");
2085 }
2086 for (const auto& value : values.commitment) {
2087 if (!value.has_value()) {
2088 return unexpected_error(ErrorCode::MissingValue, "BulletproofTranscript::assignment_data:commitment");
2089 }
2090 assignment.commitments.push_back(*value);
2091 }
2092 return assignment;
2093}
2094
2096 Result<BulletproofAssignmentData> assignment = assignment_data(vars);
2097 if (!assignment.has_value()) {
2098 return unexpected_error(assignment.error(), "BulletproofTranscript::serialize_assignment:assignment_data");
2099 }
2100 return assignment->serialize();
2101}
2102
2103bool BulletproofTranscript::is_transcript_var(Symbol symbol) {
2104 return symbol.kind == SymbolKind::Witness;
2105}
2106
2107bool BulletproofTranscript::contains_transcript_var(const Expr& expr) {
2108 return std::any_of(expr.linear().begin(), expr.linear().end(),
2109 [](const auto& term) { return is_transcript_var(term.first); });
2110}
2111
2112Expr circuit_1bit(const std::array<FieldElement, 2>& values, Transcript&, const Expr& x) {
2113 return ExprBuilder::reserved(x.linear().size())
2114 .add(values[0])
2115 .add_scaled(x, values[1] - values[0])
2116 .build();
2117}
2118
2119Expr circuit_2bit(const std::array<FieldElement, 4>& values, Transcript& transcript, const Expr& x, const Expr& y) {
2120 Expr xy = transcript.mul(x, y);
2121 return ExprBuilder::reserved(x.linear().size() + y.linear().size() + xy.linear().size())
2122 .add(values[0])
2123 .add_scaled(x, values[1] - values[0])
2124 .add_scaled(y, values[2] - values[0])
2125 .add_scaled(xy, values[0] + values[3] - values[1] - values[2])
2126 .build();
2127}
2128
2129Expr circuit_3bit(const std::array<FieldElement, 8>& values, Transcript& transcript, const Expr& x, const Expr& y, const Expr& z) {
2130 Expr xy = transcript.mul(x, y);
2131 Expr yz = transcript.mul(y, z);
2132 Expr zx = transcript.mul(z, x);
2133 Expr xyz = transcript.mul(xy, z);
2134 return ExprBuilder::reserved(x.linear().size() + y.linear().size() + z.linear().size()
2135 + xy.linear().size() + yz.linear().size() + zx.linear().size() + xyz.linear().size())
2136 .add(values[0])
2137 .add_scaled(x, values[1] - values[0])
2138 .add_scaled(y, values[2] - values[0])
2139 .add_scaled(z, values[4] - values[0])
2140 .add_scaled(xy, values[0] + values[3] - values[1] - values[2])
2141 .add_scaled(zx, values[0] + values[5] - values[1] - values[4])
2142 .add_scaled(yz, values[0] + values[6] - values[2] - values[4])
2143 .add_scaled(xyz, values[1] + values[2] + values[4] + values[7] - values[0] - values[3] - values[5] - values[6])
2144 .build();
2145}
2146
2147ExprPoint circuit_1bit_point(const EllipticCurve& curve, const std::array<JacobianPoint, 2>& points,
2148 Transcript& transcript, const Expr& b0) {
2149 std::array<AffinePoint, 2> affine_points{curve.affine(points[0]), curve.affine(points[1])};
2150 return {
2151 circuit_1bit({affine_points[0].x, affine_points[1].x}, transcript, b0),
2152 circuit_1bit({affine_points[0].y, affine_points[1].y}, transcript, b0)
2153 };
2154}
2155
2156ExprPoint circuit_2bit_point(const EllipticCurve& curve, const std::array<JacobianPoint, 4>& points,
2157 Transcript& transcript, const Expr& b0, const Expr& b1) {
2158 std::array<AffinePoint, 4> affine_points{curve.affine(points[0]), curve.affine(points[1]), curve.affine(points[2]), curve.affine(points[3])};
2159 return {
2160 circuit_2bit({affine_points[0].x, affine_points[1].x, affine_points[2].x, affine_points[3].x}, transcript, b0, b1),
2161 circuit_2bit({affine_points[0].y, affine_points[1].y, affine_points[2].y, affine_points[3].y}, transcript, b0, b1)
2162 };
2163}
2164
2165ExprPoint circuit_3bit_point(const EllipticCurve& curve, const std::array<JacobianPoint, 8>& points,
2166 Transcript& transcript, const Expr& b0, const Expr& b1, const Expr& b2) {
2167 std::array<AffinePoint, 8> affine_points{
2168 curve.affine(points[0]), curve.affine(points[1]), curve.affine(points[2]), curve.affine(points[3]),
2169 curve.affine(points[4]), curve.affine(points[5]), curve.affine(points[6]), curve.affine(points[7])
2170 };
2171 return {
2172 circuit_3bit({affine_points[0].x, affine_points[1].x, affine_points[2].x, affine_points[3].x,
2173 affine_points[4].x, affine_points[5].x, affine_points[6].x, affine_points[7].x},
2174 transcript, b0, b1, b2),
2175 circuit_3bit({affine_points[0].y, affine_points[1].y, affine_points[2].y, affine_points[3].y,
2176 affine_points[4].y, affine_points[5].y, affine_points[6].y, affine_points[7].y},
2177 transcript, b0, b1, b2)
2178 };
2179}
2180
2181ExprPoint circuit_optionally_negate_ec(const ExprPoint& point, Transcript& transcript, const Expr& negate_bit) {
2182 return {point.first, transcript.mul(Expr(1) - 2 * negate_bit, point.second)};
2183}
2184
2185ExprPoint circuit_ec_add(Transcript& transcript, const ExprPoint& p1, const ExprPoint& p2) {
2186 Expr lambda = transcript.div(p2.second - p1.second, p2.first - p1.first);
2187 Expr lambda_sq = transcript.mul(lambda, lambda);
2188 Expr x = ExprBuilder::reserved(lambda_sq.linear().size() + p1.first.linear().size() + p2.first.linear().size())
2189 .add(lambda_sq)
2190 .subtract(p1.first)
2191 .subtract(p2.first)
2192 .build();
2193 Expr delta = ExprBuilder::reserved(p1.first.linear().size() + x.linear().size())
2194 .add(p1.first)
2195 .subtract(x)
2196 .build();
2197 Expr lambda_delta = transcript.mul(lambda, delta);
2198 Expr y = ExprBuilder::reserved(lambda_delta.linear().size() + p1.second.linear().size())
2199 .add(lambda_delta)
2200 .subtract(p1.second)
2201 .build();
2202 return {x, y};
2203}
2204
2205Expr circuit_ec_add_x(Transcript& transcript, const ExprPoint& p1, const ExprPoint& p2) {
2206 Expr lambda = transcript.div(p2.second - p1.second, p2.first - p1.first);
2207 Expr lambda_sq = transcript.mul(lambda, lambda);
2208 return ExprBuilder::reserved(lambda_sq.linear().size() + p1.first.linear().size() + p2.first.linear().size())
2209 .add(lambda_sq)
2210 .subtract(p1.first)
2211 .subtract(p2.first)
2212 .build();
2213}
2214
2216 const JacobianPoint& point, const std::vector<Expr>& bits) {
2217 std::vector<JacobianPoint> powers;
2218 powers.reserve(bits.size());
2219 powers.push_back(point);
2220 for (std::size_t i = 1; i < bits.size(); ++i) {
2221 powers.push_back(curve.double_point(powers.back()));
2222 }
2223
2224 std::vector<ExprPoint> lookups;
2225 for (std::size_t i = 0; i < (bits.size() - 1) / 3; ++i) {
2226 JacobianPoint p1 = powers[i * 3];
2227 JacobianPoint p3 = curve.add(p1, powers[i * 3 + 1]);
2228 JacobianPoint p5 = curve.add(p3, powers[i * 3 + 1]);
2229 JacobianPoint p7 = curve.add(p5, powers[i * 3 + 1]);
2230 lookups.push_back(circuit_optionally_negate_ec(
2231 circuit_2bit_point(curve, {p1, p3, p5, p7}, transcript, bits[i * 3 + 1], bits[i * 3 + 2]),
2232 transcript,
2233 bits[i * 3 + 3]));
2234 }
2235
2236 if (bits.size() % 3 == 0) {
2237 JacobianPoint pn = powers[powers.size() - 3];
2238 JacobianPoint p3n = curve.add(pn, powers[powers.size() - 2]);
2239 JacobianPoint p5n = curve.add(p3n, powers[powers.size() - 2]);
2240 JacobianPoint p7n = curve.add(p5n, powers[powers.size() - 2]);
2241 JacobianPoint pn1 = curve.add(pn, powers[0]);
2242 JacobianPoint p3n1 = curve.add(p3n, powers[0]);
2243 JacobianPoint p5n1 = curve.add(p5n, powers[0]);
2244 JacobianPoint p7n1 = curve.add(p7n, powers[0]);
2245 lookups.push_back(circuit_3bit_point(curve, {pn, pn1, p3n, p3n1, p5n, p5n1, p7n, p7n1},
2246 transcript, bits[0], bits[bits.size() - 2], bits[bits.size() - 1]));
2247 } else if (bits.size() % 3 == 1) {
2248 JacobianPoint pn = powers.back();
2249 JacobianPoint pn1 = curve.add(pn, powers[0]);
2250 lookups.push_back(circuit_1bit_point(curve, {pn, pn1}, transcript, bits[0]));
2251 } else {
2252 JacobianPoint pn = powers[powers.size() - 2];
2253 JacobianPoint p3n = curve.add(pn, powers.back());
2254 JacobianPoint pn1 = curve.add(pn, powers[0]);
2255 JacobianPoint p3n1 = curve.add(p3n, powers[0]);
2256 lookups.push_back(circuit_2bit_point(curve, {pn, pn1, p3n, p3n1}, transcript, bits[0], bits.back()));
2257 }
2258
2259 ExprPoint out = lookups[0];
2260 for (std::size_t i = 1; i + 1 < lookups.size(); ++i) {
2261 out = circuit_ec_add(transcript, out, lookups[i]);
2262 }
2263 return circuit_ec_add_x(transcript, out, lookups.back());
2264}
2265
2266Expr circuit_combine(Transcript& transcript, const Expr& x1, const Expr& x2) {
2267 Expr v = ExprBuilder::reserved(x2.linear().size())
2268 .add_scaled(x2, field_di())
2269 .build();
2270 Expr u_plus_v = ExprBuilder::reserved(x1.linear().size() + v.linear().size())
2271 .add(x1)
2272 .add(v)
2273 .build();
2274 Expr uv = transcript.mul(x1, v);
2275 Expr uv_plus_a = ExprBuilder::reserved(uv.linear().size())
2276 .add(field_a())
2277 .add(uv)
2278 .build();
2279 Expr numerator_mul = transcript.mul(u_plus_v, uv_plus_a);
2280 Expr numerator = ExprBuilder::reserved(numerator_mul.linear().size())
2282 .add(numerator_mul)
2283 .build();
2284 Expr u_minus_v = ExprBuilder::reserved(x1.linear().size() + v.linear().size())
2285 .add(x1)
2286 .subtract(v)
2287 .build();
2288 Expr denominator = transcript.mul(u_minus_v, u_minus_v);
2289 return transcript.div(numerator, denominator);
2290}
2291
2293 const std::optional<UInt256>& z1, const std::optional<UInt256>& z2) {
2294 int z1_bits_len = static_cast<int>(half_n1().bit_length());
2295 int z2_bits_len = static_cast<int>(half_n2().bit_length());
2296 std::vector<int> z1_values(static_cast<std::size_t>(z1_bits_len), -1);
2297 std::vector<int> z2_values(static_cast<std::size_t>(z2_bits_len), -1);
2298 if (z1.has_value() && z2.has_value()) {
2299 Result<std::vector<int>> z1_result = key_to_bits(*z1, half_n1());
2300 if (!z1_result.has_value()) {
2301 return unexpected_error(z1_result.error(), "circuit_main:key_to_bits_z1");
2302 }
2303 Result<std::vector<int>> z2_result = key_to_bits(*z2, half_n2());
2304 if (!z2_result.has_value()) {
2305 return unexpected_error(z2_result.error(), "circuit_main:key_to_bits_z2");
2306 }
2307 z1_values = *z1_result;
2308 z2_values = *z2_result;
2309 }
2310 std::vector<Expr> z1_bits;
2311 std::vector<Expr> z2_bits;
2312 z1_bits.reserve(z1_values.size());
2313 z2_bits.reserve(z2_values.size());
2314 for (int bit : z1_values) {
2315 z1_bits.push_back(transcript.boolean(transcript.secret(bit < 0 ? std::nullopt : std::optional<FieldElement>(FieldElement::from_int(bit)))));
2316 }
2317 for (int bit : z2_values) {
2318 z2_bits.push_back(transcript.boolean(transcript.secret(bit < 0 ? std::nullopt : std::optional<FieldElement>(FieldElement::from_int(bit)))));
2319 }
2320 std::size_t n_bits = z1_bits.size() + z2_bits.size();
2321 Expr out_p1x = circuit_ec_multiply_x(curve1(), transcript, generator1(), z1_bits);
2322 Expr out_p2x = circuit_ec_multiply_x(curve2(), transcript, generator2(), z2_bits);
2323 Expr out_x1 = circuit_ec_multiply_x(curve1(), transcript, m1, z1_bits);
2324 Expr out_x2 = circuit_ec_multiply_x(curve2(), transcript, m2, z2_bits);
2325 return CircuitMainResult{circuit_combine(transcript, out_x1, out_x2), out_p1x, out_p2x, n_bits};
2326}
2327
2328} // namespace
2329
2330namespace purify {
2331
2333 const NativeBulletproofCircuit& circuit,
2334 std::span<const unsigned char> statement_binding) {
2335 return ::circuit_binding_digest(circuit, statement_binding);
2336}
2337
2340 std::span<const unsigned char> statement_binding) {
2341 return ::circuit_binding_digest(circuit, statement_binding);
2342}
2343
2344} // namespace purify
int purify_bulletproof_prove_circuit(purify_secp_context *context, const purify_bulletproof_circuit_view *circuit, const purify_bulletproof_assignment_view *assignment, const unsigned char *blind32, const unsigned char value_gen33[33], const unsigned char nonce32[32], const unsigned char *extra_commit, size_t extra_commit_len, unsigned char commitment_out33[33], unsigned char *proof_out, size_t *proof_len)
int purify_bulletproof_prove_circuit_with_resources(purify_bulletproof_backend_resources *resources, const purify_bulletproof_circuit_view *circuit, const purify_bulletproof_assignment_view *assignment, const unsigned char *blind32, const unsigned char value_gen33[33], const unsigned char nonce32[32], const unsigned char *extra_commit, size_t extra_commit_len, unsigned char commitment_out33[33], unsigned char *proof_out, size_t *proof_len)
int purify_bulletproof_verify_circuit(purify_secp_context *context, const purify_bulletproof_circuit_view *circuit, const unsigned char commitment33[33], const unsigned char value_gen33[33], const unsigned char *extra_commit, size_t extra_commit_len, const unsigned char *proof, size_t proof_len)
int purify_bulletproof_prove_circuit_assume_valid(purify_secp_context *context, const purify_bulletproof_circuit_view *circuit, const purify_bulletproof_assignment_view *assignment, const unsigned char *blind32, const unsigned char value_gen33[33], const unsigned char nonce32[32], const unsigned char *extra_commit, size_t extra_commit_len, unsigned char commitment_out33[33], unsigned char *proof_out, size_t *proof_len)
int purify_bulletproof_verify_circuit_with_resources(purify_bulletproof_backend_resources *resources, const purify_bulletproof_circuit_view *circuit, const unsigned char commitment33[33], const unsigned char value_gen33[33], const unsigned char *extra_commit, size_t extra_commit_len, const unsigned char *proof, size_t proof_len)
int purify_bulletproof_prove_circuit_assume_valid_with_resources(purify_bulletproof_backend_resources *resources, const purify_bulletproof_circuit_view *circuit, const purify_bulletproof_assignment_view *assignment, const unsigned char *blind32, const unsigned char value_gen33[33], const unsigned char nonce32[32], const unsigned char *extra_commit, size_t extra_commit_len, unsigned char commitment_out33[33], unsigned char *proof_out, size_t *proof_len)
void purify_bulletproof_backend_resources_destroy(purify_bulletproof_backend_resources *resources)
purify_bulletproof_backend_resources * purify_bulletproof_backend_resources_create(purify_secp_context *context, size_t n_gates)
purify_bulletproof_backend_resources * purify_bulletproof_backend_resources_clone(const purify_bulletproof_backend_resources *resources)
size_t purify_bulletproof_required_proof_size(size_t n_gates)
C ABI bridging Purify C++ code to secp256k1-zkp BPPP functionality.
Native Bulletproof-style circuit types and witness serialization helpers.
bool replace_and_insert(Expr &expr, Symbol symbol)
Detects simple witness aliases and records them for later assignment lowering.
bool evaluate(const WitnessAssignments &vars, const FieldElement &commitment) const
Evaluates the lowered symbolic constraints with a concrete commitment value.
std::string to_string() const
Renders the lowered circuit in the legacy textual verifier format.
void add_assignment(Symbol symbol, Expr expr)
Adds one symbolic wire assignment to the lowering state.
Status from_transcript(const Transcript &transcript, std::size_t n_bits)
Imports a symbolic transcript and pads it to a power-of-two multiplication count.
void replace_expr_v_with_bp_var(Expr &expr)
Rewrites transcript variable names to their eventual Bulletproof wire aliases.
Status add_pubkey_and_out(const UInt512 &pubkey, Expr p1x, Expr p2x, Expr out)
Binds packed public-key coordinates and the output commitment into explicit constraints.
NativeBulletproofCircuit native_circuit() const
Builds the native sparse circuit object from the lowered assignments and constraints.
Result< BulletproofAssignmentData > assignment_data(const WitnessAssignments &vars) const
Materializes the witness columns expected by the native circuit representation.
Result< Bytes > serialize_assignment(const WitnessAssignments &vars) const
Serializes the derived witness assignment using the legacy blob format.
Minimal elliptic-curve arithmetic over the Purify base field.
Definition curve.hpp:46
AffinePoint affine(const JacobianPoint &point) const
Converts a Jacobian point to affine coordinates.
Definition curve.cpp:87
JacobianPoint add(const JacobianPoint &lhs, const JacobianPoint &rhs) const
Adds two Jacobian points.
Definition curve.cpp:135
JacobianPoint double_point(const JacobianPoint &point) const
Doubles a Jacobian point.
Definition curve.cpp:118
Purify result carrier that either holds a value or an error.
Definition expected.hpp:64
bool has_value() const noexcept
Definition expected.hpp:170
Caller-owned cache for reusable legacy Bulletproof backend resources keyed by gate count.
ExperimentalBulletproofBackendCache & operator=(const ExperimentalBulletproofBackendCache &)=delete
purify_bulletproof_backend_resources * get_or_create(std::size_t n_gates, purify_secp_context *secp_context)
Returns cached backend resources for this gate count, creating them on first use.
Result< ExperimentalBulletproofBackendCache > clone_for_thread(std::size_t n_gates) const
Clones one warmed backend-resource entry for independent use on another thread.
ExprBuilder & add(const FieldElement &value)
Adds a constant field term to the pending affine expression.
Definition expr.cpp:171
static ExprBuilder reserved(std::size_t terms)
Returns a builder with storage reserved for approximately terms linear slots.
Definition expr.cpp:160
Expr build()
Materializes the flattened affine expression.
Definition expr.cpp:242
ExprBuilder & add_scaled(const Expr &expr, const FieldElement &scale)
Adds an existing expression scaled by a field element.
Definition expr.cpp:214
ExprBuilder & subtract(const Expr &expr)
Subtracts an existing expression with implicit coefficient minus one.
Definition expr.cpp:199
Symbolic affine expression over indexed variables and field coefficients.
Definition expr.hpp:71
const FieldElement & constant() const
Returns the constant term of the affine expression.
Definition expr.hpp:86
std::vector< Term > & linear()
Returns mutable access to the sorted linear term list.
Definition expr.hpp:91
static Expr variable(Symbol symbol)
Returns a single-variable expression with coefficient one.
Definition expr.cpp:98
Field element modulo the backend scalar field used by this implementation.
Definition numeric.hpp:815
static Result< FieldElement > try_from_uint256(const UInt256 &value)
Converts a canonical 256-bit unsigned integer into the scalar field representation.
Definition numeric.cpp:69
static FieldElement one()
Returns the multiplicative identity of the scalar field.
Definition numeric.cpp:36
FieldElement negate() const
Returns the additive inverse modulo the field prime.
Definition numeric.cpp:121
static FieldElement from_int(std::int64_t value)
Constructs a field element from a signed integer, reducing negatives modulo the field.
Definition numeric.cpp:46
bool is_zero() const
Returns true when the element is zero.
Definition numeric.cpp:104
static FieldElement zero()
Returns the additive identity of the scalar field.
Definition numeric.cpp:32
std::array< unsigned char, 32 > to_bytes_be() const
Serializes the field element in big-endian form.
Definition numeric.cpp:84
Public-key-agnostic native verifier-circuit template.
Result< NativeBulletproofCircuit::PackedWithSlack > instantiate_packed(const UInt512 &pubkey) const
Result< NativeBulletproofCircuit > instantiate(const UInt512 &pubkey) const
Result< bool > partial_evaluate(const BulletproofAssignmentData &assignment) const
Evaluates only the public-key-agnostic base circuit cached inside the template.
Result< Bytes > integrity_digest() const
Returns a stable digest of the packed base circuit plus late-bound expressions.
static NativeBulletproofCircuitTemplate from_parts(NativeBulletproofCircuit::PackedWithSlack base_packed, Expr p1x, Expr p2x, Expr out)
Builds a template from a lowered base circuit plus late-bound public expressions.
Result< bool > final_evaluate(const BulletproofAssignmentData &assignment, const UInt512 &pubkey) const
Evaluates only the late-bound public-key/output constraints against one assignment.
Resettable packed circuit representation backed by one aligned owning slab.
void add_right_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the right-wire matrix.
PackedWithSlack & operator=(const PackedWithSlack &other)
Result< NativeBulletproofCircuit > unpack() const
Materializes the packed circuit back into the ergonomic row-vector representation.
void add_commitment_term(std::size_t commitment_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the commitment matrix using the Bulletproof sign convention.
void add_output_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the output-wire matrix.
void reset() noexcept
Restores the packed circuit to its original base row sizes and constraint count.
NativeBulletproofCircuitRow::PackedWithSlack right_row(std::size_t gate_idx) const noexcept
Returns a read-only packed right-wire row view.
void add_left_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the left-wire matrix.
NativeBulletproofCircuitRow::PackedWithSlack left_row(std::size_t gate_idx) const noexcept
Returns a read-only packed left-wire row view.
NativeBulletproofCircuitRow::PackedWithSlack output_row(std::size_t gate_idx) const noexcept
Returns a read-only packed output-wire row view.
std::span< const FieldElement > constants() const noexcept
Returns the live constraint constants stored in the slab.
NativeBulletproofCircuitRow::PackedWithSlack commitment_row(std::size_t commitment_idx) const noexcept
Returns a read-only packed commitment row view.
std::size_t add_constraint(const FieldElement &constant=FieldElement::zero())
Appends a new linear constraint constant term and returns its index.
static Result< PackedWithSlack > from_circuit(const NativeBulletproofCircuit &circuit, const PackedSlackPlan &slack)
Packs a native circuit into one aligned slab with caller-supplied row and constraint slack.
bool evaluate(const BulletproofAssignmentData &assignment) const
Evaluates the packed circuit directly against a witness assignment.
Reusable BIP340-style tagged SHA-256 helper.
Definition curve.hpp:111
std::array< unsigned char, 32 > digest(std::span< const unsigned char > data) const
Definition curve.hpp:117
Mutable transcript used to record symbolic multiplication, division, and boolean constraints.
Definition expr.hpp:213
const std::vector< Expr > & eqs() const
Returns the linear equality constraints accumulated so far.
Definition expr.hpp:251
Expr secret(const std::optional< FieldElement > &value)
Allocates a new secret witness variable, optionally with a known concrete value.
Definition expr.cpp:378
Expr boolean(const Expr &expr)
Constrains an expression to be boolean by adding x * (x - 1) = 0.
Definition expr.cpp:428
const std::vector< MulConstraint > & muls() const
Returns the multiplication and division constraints accumulated so far.
Definition expr.hpp:246
Expr mul(const Expr &lhs, const Expr &rhs)
Allocates or reuses a multiplication witness enforcing lhs * rhs = out.
Definition expr.cpp:386
const WitnessAssignments & varmap() const
Returns the underlying witness assignment vector.
Definition expr.hpp:234
Expr div(const Expr &lhs, const Expr &rhs)
Allocates or reuses a division witness enforcing out * rhs = lhs.
Definition expr.cpp:409
#define PURIFY_RETURN_IF_ERROR(expr, context)
Evaluates an expected-like expression and returns the wrapped error on failure.
Definition error.hpp:329
#define PURIFY_ASSIGN_OR_RETURN(lhs, expr, context)
Evaluates an expected-like expression, binds the value to lhs, and propagates errors.
Definition error.hpp:338
std::optional< std::uint32_t > read_u32_le(std::span< const unsigned char > bytes, std::size_t offset)
Definition common.hpp:35
void append_u32_le(Bytes &out, std::uint32_t value)
Definition common.hpp:29
std::size_t circuit_n_gates(const NativeBulletproofCircuit &circuit)
Definition common.hpp:47
std::size_t circuit_n_commitments(const NativeBulletproofCircuit &circuit)
Definition common.hpp:55
Definition api.hpp:21
Status require_secp_context(const purify_secp_context *context, const char *error_context)
Definition common.hpp:56
FieldElement field_di()
Returns the inverse of the twist factor in the field.
Definition curve.cpp:251
const UInt256 & half_n1()
Returns floor(order_n1 / 2).
Definition curve.cpp:214
Result< Bytes > experimental_circuit_binding_digest(const NativeBulletproofCircuit &circuit, std::span< const unsigned char > statement_binding)
ExprPoint circuit_3bit_point(const EllipticCurve &curve, const std::array< JacobianPoint, 8 > &points, Transcript &transcript, const Expr &b0, const Expr &b1, const Expr &b2)
Selects between eight affine point constants using three boolean expression bits.
constexpr Unexpected< Error > unexpected_error(ErrorCode code, const char *context=nullptr)
Constructs an unexpected Error value from a machine-readable code.
Definition error.hpp:293
Result< std::vector< int > > key_to_bits(UInt256 n, const UInt256 &max_value)
Encodes a scalar into the signed 3-bit window bit schedule used by the circuit.
Definition curve.cpp:353
bool size_fits_u64(std::size_t value) noexcept
Definition common.hpp:83
bool is_power_of_two_size(std::size_t value) noexcept
Definition common.hpp:94
Result< std::uint64_t > narrow_size_to_u64(std::size_t value, const char *context)
Definition common.hpp:87
Result< std::pair< UInt256, UInt256 > > unpack_public(const UInt512 &packed)
Splits a packed public key into its two x-coordinates.
Definition curve.cpp:330
ErrorCode
Machine-readable error codes shared across the library.
Definition error.hpp:42
const UInt256 & half_n2()
Returns floor(order_n2 / 2).
Definition curve.cpp:219
std::pair< Expr, Expr > ExprPoint
Symbolic affine point represented as independent x and y expressions.
bool checked_mul_size(std::size_t lhs, std::size_t rhs, std::size_t &out) noexcept
Definition common.hpp:71
Expr circuit_combine(Transcript &transcript, const Expr &x1, const Expr &x2)
Builds the symbolic Purify output combiner over two x-coordinates.
Result< bool > verify_experimental_circuit(const NativeBulletproofCircuit &circuit, const ExperimentalBulletproofProof &proof, const BulletproofGeneratorBytes &value_generator, purify_secp_context *secp_context, std::span< const unsigned char > statement_binding={}, ExperimentalBulletproofBackendCache *backend_cache=nullptr)
Verifies a proof produced by prove_experimental_circuit against the same one-commitment native circui...
ExprPoint circuit_ec_add(Transcript &transcript, const ExprPoint &p1, const ExprPoint &p2)
Symbolically adds two affine elliptic-curve points.
std::unique_ptr< purify_bulletproof_backend_resources, BulletproofBackendResourceDeleter > BulletproofBackendResourcePtr
FieldElement field_a()
Returns the shared Weierstrass a coefficient used by Purify.
Definition curve.cpp:239
Expr circuit_ec_add_x(Transcript &transcript, const ExprPoint &p1, const ExprPoint &p2)
Symbolically adds two affine points and returns only the resulting x-coordinate.
const EllipticCurve & curve1()
Returns the first Purify curve instance.
Definition curve.cpp:256
Expr circuit_ec_multiply_x(const EllipticCurve &curve, Transcript &transcript, const JacobianPoint &point, const std::vector< Expr > &bits)
Builds the symbolic x-coordinate multiplication gadget for one curve point.
Result< CircuitMainResult > circuit_main(Transcript &transcript, const JacobianPoint &m1, const JacobianPoint &m2, const std::optional< UInt256 > &z1=std::nullopt, const std::optional< UInt256 > &z2=std::nullopt)
Builds the full symbolic Purify circuit from message points and optional witness scalars.
void best_effort_reserve_mul(std::vector< T > &out, std::size_t lhs, std::size_t rhs)
Definition common.hpp:115
std::vector< unsigned char > Bytes
Dynamically sized byte string used for messages, serialized witnesses, and proofs.
Definition common.hpp:99
std::array< unsigned char, 33 > BulletproofGeneratorBytes
const JacobianPoint & generator1()
Returns the fixed generator for the first curve.
Definition curve.cpp:277
SymbolKind
Symbol classes used while deriving witness and Bulletproof wire relations.
Definition expr.hpp:26
constexpr std::string_view to_string(ErrorCategory category) noexcept
Returns a stable programmatic name for an error category.
Definition error.hpp:146
Result< ExperimentalBulletproofProof > prove_experimental_circuit(const NativeBulletproofCircuit &circuit, const BulletproofAssignmentData &assignment, const BulletproofScalarBytes &nonce, const BulletproofGeneratorBytes &value_generator, purify_secp_context *secp_context, std::span< const unsigned char > statement_binding={}, std::optional< BulletproofScalarBytes > blind=std::nullopt, ExperimentalBulletproofBackendCache *backend_cache=nullptr)
Proves a native circuit with the experimental imported Bulletproof circuit backend.
Expr circuit_1bit(const std::array< FieldElement, 2 > &values, Transcript &transcript, const Expr &x)
Selects one of two field constants using a single boolean expression bit.
FieldElement field_b()
Returns the shared Weierstrass b coefficient used by Purify.
Definition curve.cpp:243
std::array< unsigned char, 33 > BulletproofPointBytes
bool size_fits_u32(std::size_t value) noexcept
Definition common.hpp:79
const EllipticCurve & curve2()
Returns the second Purify curve instance.
Definition curve.cpp:261
const JacobianPoint & generator2()
Returns the fixed generator for the second curve.
Definition curve.cpp:288
Expr circuit_3bit(const std::array< FieldElement, 8 > &values, Transcript &transcript, const Expr &x, const Expr &y, const Expr &z)
Selects one of eight field constants using three boolean expression bits.
std::array< unsigned char, 32 > BulletproofScalarBytes
Result< ExperimentalBulletproofProof > prove_experimental_circuit_assume_valid(const NativeBulletproofCircuit::PackedWithSlack &circuit, const BulletproofAssignmentData &assignment, const BulletproofScalarBytes &nonce, const BulletproofGeneratorBytes &value_generator, purify_secp_context *secp_context, std::span< const unsigned char > statement_binding, std::optional< BulletproofScalarBytes > blind, ExperimentalBulletproofBackendCache *backend_cache)
ExprPoint circuit_1bit_point(const EllipticCurve &curve, const std::array< JacobianPoint, 2 > &points, Transcript &transcript, const Expr &b0)
Selects between two affine point constants using one boolean expression bit.
ExprPoint circuit_optionally_negate_ec(const ExprPoint &point, Transcript &transcript, const Expr &negate_bit)
Conditionally negates an elliptic-curve point encoded as symbolic affine expressions.
bool checked_add_size(std::size_t lhs, std::size_t rhs, std::size_t &out) noexcept
Definition common.hpp:63
std::vector< std::optional< FieldElement > > WitnessAssignments
Partial witness assignment vector indexed by transcript witness id.
Definition expr.hpp:63
Expected< void, Error > Status
Expected-returning convenience alias for Purify status-only APIs.
Definition error.hpp:102
ExprPoint circuit_2bit_point(const EllipticCurve &curve, const std::array< JacobianPoint, 4 > &points, Transcript &transcript, const Expr &b0, const Expr &b1)
Selects between four affine point constants using two boolean expression bits.
Expr circuit_2bit(const std::array< FieldElement, 4 > &values, Transcript &transcript, const Expr &x, const Expr &y)
Selects one of four field constants using two boolean expression bits.
Scalar32 scalar
Definition bppp.cpp:119
Nonce nonce
Definition bppp.cpp:120
XOnly32 binding_digest
Definition bppp.cpp:122
std::size_t bit_length() const
Returns the index of the highest set bit plus one.
Definition numeric.hpp:474
Columnar witness assignment compatible with the native Bulletproof circuit layout.
std::vector< FieldElement > output
std::vector< FieldElement > commitments
std::vector< FieldElement > right
Result< Bytes > serialize() const
Serializes the witness columns in the legacy assignment blob format.
std::vector< FieldElement > left
void operator()(purify_bulletproof_backend_resources *resources) const noexcept
Result bundle returned by the main symbolic Purify circuit construction.
std::unordered_map< std::size_t, BulletproofBackendResourcePtr > resources
Experimental single-proof wrapper over the imported legacy Bulletproof circuit backend.
static Result< ExperimentalBulletproofProof > deserialize(std::span< const unsigned char > bytes)
Jacobian point representation used for curve arithmetic.
Definition curve.hpp:17
Non-owning packed row view used by NativeBulletproofCircuit::PackedWithSlack.
void add(std::size_t idx, const FieldElement &scalar)
Appends a sparse coefficient to the row, skipping zero entries.
std::vector< NativeBulletproofCircuitTerm > entries
One sparse matrix entry in a native circuit row.
Native in-memory representation of a Bulletproof-style arithmetic circuit.
std::size_t add_constraint(const FieldElement &constant=FieldElement::zero())
Appends a new linear constraint constant term and returns its index.
std::vector< NativeBulletproofCircuitRow > wv
void resize(std::size_t gates, std::size_t commitments, std::size_t bits=0)
Reinitializes the circuit shape and clears all accumulated constraints.
bool evaluate(const BulletproofAssignmentData &assignment) const
Evaluates the circuit against a witness assignment and checks all gate and row equations.
std::vector< NativeBulletproofCircuitRow > wr
bool has_valid_shape() const
Returns true when the sparse matrix vectors match the declared circuit dimensions.
std::vector< NativeBulletproofCircuitRow > wl
void add_left_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the left-wire matrix.
std::vector< NativeBulletproofCircuitRow > wo
Result< PackedWithSlack > pack_with_slack() const
Packs the circuit into one aligned slab with no additional row or constraint slack.
void add_right_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the right-wire matrix.
void add_output_term(std::size_t gate_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the output-wire matrix.
std::vector< FieldElement > c
void add_commitment_term(std::size_t commitment_idx, std::size_t constraint_idx, const FieldElement &scalar)
Adds a coefficient to the commitment matrix using the Bulletproof sign convention.
Compact symbolic variable identifier used inside expressions and transcripts.
Definition expr.hpp:35
static Symbol witness(std::uint32_t index)
Definition expr.cpp:55
static Symbol left(std::uint32_t index)
Definition expr.cpp:59
static Symbol output(std::uint32_t index)
Definition expr.cpp:67
std::uint32_t index
Definition expr.hpp:37
static Symbol commitment(std::uint32_t index)
Definition expr.cpp:71
static Symbol right(std::uint32_t index)
Definition expr.cpp:63
SymbolKind kind
Definition expr.hpp:36
int PURIFY_UINT_FN() bit(const uint64_t value[PURIFY_UINT_WORDS], size_t index)
Definition uint_impl.h:125