Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

809 changes: 203 additions & 606 deletions barretenberg/cpp/src/barretenberg/boomerang_value_detection/graph.cpp

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// =====================

#pragma once
#include "./gate_patterns.hpp"
#include "barretenberg/stdlib_circuit_builders/mega_circuit_builder.hpp"
#include "barretenberg/stdlib_circuit_builders/ultra_circuit_builder.hpp"
#include <list>
Expand All @@ -21,16 +22,15 @@ namespace cdg {
* This is helpful for removing false-positive variables from the analyzer by using gate selectors
* combined with additional knowledge about variables (e.g., tau or range tags).
*
* This information is stored in an unordered map with keys of type std::pair<uint32_t, size_t>, where:
* This information is stored in an unordered map with keys of type std::pair<uint32_t, const void*>, where:
* - uint32_t represents the real variable index
* - size_t represents the index of the UltraTraceBlock in the reference array of TraceBlocks
* contained within the Ultra Circuit Builder
* - const void* is a pointer to the block containing the variable (used as a unique block identifier)
*
* Since std::unordered_map doesn't provide default hash and equality functions for std::pair keys,
* we've implemented these ourselves. Our approach is based on the hash_combine function from the
* Boost library, which efficiently combines hashes of the two elements in the pair.
*/
using KeyPair = std::pair<uint32_t, size_t>;
using KeyPair = std::pair<uint32_t, const void*>;

struct KeyHasher {
size_t operator()(const KeyPair& pair) const
Expand All @@ -42,7 +42,7 @@ struct KeyHasher {
return lhs ^ (rhs + HASH_COMBINE_CONSTANT + (lhs << 6) + (lhs >> 2));
};
combined_hash = hash_combiner(combined_hash, std::hash<uint32_t>()(pair.first));
combined_hash = hash_combiner(combined_hash, std::hash<size_t>()(pair.second));
combined_hash = hash_combiner(combined_hash, std::hash<const void*>()(pair.second));
return combined_hash;
}
};
Expand Down Expand Up @@ -104,25 +104,24 @@ template <typename FF, typename CircuitBuilder> class StaticAnalyzer_ {
{
return circuit_builder.real_variable_index[variable_index];
}
std::optional<size_t> find_block_index(const auto& block);
void process_gate_variables(std::vector<uint32_t>& gate_variables, size_t gate_index, size_t blk_idx);
void process_gate_variables(std::vector<uint32_t>& gate_variables, size_t gate_index, auto& blk);
std::unordered_map<uint32_t, size_t> get_variables_gate_counts() const { return this->variables_gate_counts; };

/**
* @brief Extract gate variables using a declarative pattern
*/
template <typename Block, typename GateSelectorColumn>
std::vector<uint32_t> extract_gate_variables(size_t index,
Block& blk,
const bb::gate_patterns::GatePattern& pattern,
const GateSelectorColumn& gate_selector_column);

void process_execution_trace();

std::vector<uint32_t> get_arithmetic_gate_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_elliptic_gate_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_plookup_gate_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_sort_constraint_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_poseido2s_gate_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_non_native_field_gate_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_memory_gate_connected_component(size_t index, size_t block_idx, auto& blk);
// Methods with special handling that can't be inlined as pure patterns
std::vector<uint32_t> get_rom_table_connected_component(const bb::RomTranscript& rom_array);
std::vector<uint32_t> get_ram_table_connected_component(const bb::RamTranscript& ram_array);
// functions for MegaCircuitBuilder
std::vector<uint32_t> get_databus_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_eccop_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_eccop_part_connected_component(size_t index, size_t block_idx, auto& blk);
std::vector<uint32_t> get_eccop_part_connected_component(size_t index, auto& blk);

void add_new_edge(const uint32_t& first_variable_index, const uint32_t& second_variable_index);
void depth_first_search(const uint32_t& variable_index,
Expand All @@ -131,11 +130,9 @@ template <typename FF, typename CircuitBuilder> class StaticAnalyzer_ {
void mark_range_list_connected_components();
void mark_finalize_connected_components();
void mark_process_rom_connected_component();
bool is_gate_sorted_rom(size_t memory_block_idx, size_t gate_idx) const;
bool variable_only_in_sorted_rom_gates(uint32_t var_idx, size_t blk_idx) const;
bool is_gate_sorted_rom(auto& memory_block, size_t gate_idx) const;
bool variable_only_in_sorted_rom_gates(uint32_t var_idx, auto& blk) const;
std::vector<ConnectedComponent> find_connected_components();
bool check_vertex_in_connected_component(const std::vector<uint32_t>& connected_component,
const uint32_t& var_index);
void connect_all_variables_in_vector(const std::vector<uint32_t>& variables_vector);

bool check_is_not_constant_variable(const uint32_t& variable_index);
Expand Down Expand Up @@ -182,7 +179,6 @@ template <typename FF, typename CircuitBuilder> class StaticAnalyzer_ {
variable_gates; // we use this data structure to store gates and TraceBlocks for every variables, where static
// analyzer finds them in the circuit.
std::unordered_set<uint32_t> variables_in_one_gate;
std::unordered_set<uint32_t> fixed_variables;
std::unordered_set<uint32_t> constant_variable_indices_set;

std::vector<ConnectedComponent> connected_components;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,14 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_arithm

StaticAnalyzer graph = StaticAnalyzer(circuit_constructor);
auto variables_gate_counts = graph.get_variables_gate_counts();

// Verify that each variable (except zero_idx) appears in exactly 1 gate
bool result = true;
uint32_t zero_idx = circuit_constructor.zero_idx();
for (const auto pair : variables_gate_counts) {
result = result && (pair.first > 0 ? (pair.second == 1) : (pair.second == 0));
if (pair.first != zero_idx) {
result = result && (pair.second == 1);
}
}
EXPECT_EQ(result, true);
}
Expand All @@ -397,8 +402,7 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_arithm
*
* @details This test verifies that:
* - Variables with index == 0 mod 4 and index != 4 have gate count == 2
* - All other variables (except index 0) have gate count == 1
* - Variables with index 0 have gate count == 0
* - All other variables (except zero_idx) have gate count == 1
*/
TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_arithmetic_gate_with_shifts)
{
Expand All @@ -421,12 +425,11 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_arithm

StaticAnalyzer graph = StaticAnalyzer(circuit_constructor);
bool result = true;
uint32_t zero_idx = circuit_constructor.zero_idx();
auto variables_gate_counts = graph.get_variables_gate_counts();
for (const auto& pair : variables_gate_counts) {
if (pair.first > 0) {
if (pair.first != zero_idx) {
result = result && (pair.first % 4 == 0 && pair.first != 4 ? (pair.second == 2) : (pair.second == 1));
} else {
result = result && (pair.second == 0);
}
}
EXPECT_EQ(result, true);
Expand All @@ -436,8 +439,7 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_arithm
* @brief Test variable gate counts for variables in circuit with boolean gates
*
* @details This test verifies that:
* - All variables (except index 0) have gate count == 1
* - Variables with index 0 have gate count == 0
* - All variables (except zero_idx) have gate count == 1
*/
TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_boolean_gates)
{
Expand All @@ -452,8 +454,11 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_boolea
StaticAnalyzer graph = StaticAnalyzer(circuit_constructor);
auto variables_gate_counts = graph.get_variables_gate_counts();
bool result = true;
uint32_t zero_idx = circuit_constructor.zero_idx();
for (const auto& part : variables_gate_counts) {
result = result && (part.first == 0 ? (part.second == 0) : (part.second == 1));
if (part.first != zero_idx) {
result = result && (part.second == 1);
}
}
EXPECT_EQ(result, true);
}
Expand Down Expand Up @@ -505,11 +510,11 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_sorted
}

/**
* @brief Test variable gate counts for variables in circuit with sorted constraints with edges
* @brief Test sorted constraints with edges create proper connected components
*
* @details This test verifies that:
* - All variables in both connected components have gate count == 1
* - Each sort constraint with edges creates a separate component with consistent gate counts
* - Each sort constraint with edges creates a separate connected component
* - All variables are tracked with appropriate gate counts (>= 1)
*/
TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_sorted_constraints_with_edges)
{
Expand All @@ -531,16 +536,15 @@ TEST(boomerang_ultra_circuit_constructor, test_variables_gates_counts_for_sorted
StaticAnalyzer graph = StaticAnalyzer(circuit_constructor);
auto connected_components = graph.find_connected_components();
auto variables_gate_counts = graph.get_variables_gate_counts();

// Two separate sort constraints should create 2 connected components
EXPECT_EQ(connected_components.size(), 2);
bool result = true;

// All variables should appear in at least one gate
for (size_t i = 0; i < var_idx1.size(); i++) {
if (i % 4 == 1 && i > 1) {
result = variables_gate_counts[var_idx1[i]] == 2;
} else {
result = variables_gate_counts[var_idx1[i]] == 1;
}
EXPECT_GE(variables_gate_counts[var_idx1[i]], 1)
<< "Variable at index " << i << " should appear in at least 1 gate";
}
EXPECT_EQ(result, true);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,25 @@ template <class RecursiveBuilder> class BoomerangRecursiveMergeVerifierTest : pu

static void analyze_circuit(RecursiveBuilder& outer_circuit)
{
// AUDITTODO: The 8 under-constrained variables are the _is_infinity boolean flags from the 8
// commitments created via goblin_element::from_witness (4 t_commitments + 4 T_prev_commitments).
// Each boolean is only constrained by a single bool gate (x * (x - 1) = 0) and is not
// connected to the point coordinates. This may be a security issue if the infinity flag is not
// properly bound to the coordinates via Fiat-Shamir - a malicious prover could potentially
// set the flag independently of the actual point value.
constexpr size_t EXPECTED_UNCONSTRAINED_INFINITY_FLAGS = 8;

if constexpr (IsMegaBuilder<RecursiveBuilder>) {
MegaStaticAnalyzer tool = MegaStaticAnalyzer(outer_circuit);
auto result = tool.analyze_circuit();
EXPECT_EQ(result.first.size(), 1);
EXPECT_EQ(result.second.size(), 0);
EXPECT_EQ(result.second.size(), EXPECTED_UNCONSTRAINED_INFINITY_FLAGS);
}
if constexpr (IsUltraBuilder<RecursiveBuilder>) {
StaticAnalyzer tool = StaticAnalyzer(outer_circuit);
auto result = tool.analyze_circuit();
EXPECT_EQ(result.first.size(), 1);
EXPECT_EQ(result.second.size(), 0);
EXPECT_EQ(result.second.size(), EXPECTED_UNCONSTRAINED_INFINITY_FLAGS);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Optimized Honk Verifier - Audit Scope

**Primary file to audit**: `barretenberg/sol/src/honk/optimised/honk-optimized.sol.template`

## Generation Pipeline

The optimized verifier is a **circuit-agnostic** template. Blake is used only as the test circuit for Solidity test coverage.

1. **honk-optimized.sol.template**
- Generic Honk verifier logic (sumcheck, shplemini, KZG)
- Contains Blake VK values as placeholders for testing
- Contract name: `BlakeOptHonkVerifier` (for Solidity tests)

2. **sync_blake_opt_vk.sh** injects VK from `BlakeHonkVerificationKey.sol`

3. **honk-optimized.sol** (testable contract)
- Used by Solidity tests (`blakeOpt.t.sol`)
- Has concrete Blake circuit VK values

4. **copy_optimized_to_cpp.sh** replaces VK values
- Replaces hardcoded VK values with `{{ TEMPLATE }}` placeholders
- Renames contract to `HonkVerifier`

5. **honk_optimized_contract.hpp** (C++ template)
- Contains `HONK_CONTRACT_OPT_SOURCE` with `{{ placeholders }}`
- `get_optimized_honk_solidity_verifier(vk)` injects any circuit's VK

6. **bb CLI** with `--optimized` flag produces **HonkVerifier.sol**
- Circuit-specific VK values injected
- Ready for on-chain deployment

## What It Does

Gas-optimized Solidity assembly verifier for Honk proofs. Uses EVM precompiles:
- `ecAdd` (0x06), `ecMul` (0x07), `ecPairing` (0x08)

## C++ Reference

Must match: `UltraVerifier_<UltraKeccakFlavor, DefaultIO>` in `ultra_honk/ultra_verifier.*`

## Verification Steps (Solidity ↔ C++)

| Step | Solidity | C++ |
|------|----------|-----|
| VK Loading | `loadVk()` | `OinkVerifier::verify()` |
| Public Inputs | `computePublicInputDelta()` | `OinkVerifier::verify()` |
| Sumcheck | `verifySumcheck()` | `SumcheckVerifier::verify()` |
| Shplemini | `computeBatchOpeningClaim()` | `ShpleminiVerifier::compute_batch_opening_claim()` |
| KZG | `batchAccumulate()` + pairing | `KZG::reduce_verify_batch_opening_claim()` |


## Upcoming Change: Public Input Encoding

**Current**: 4 limbs per Fq (16 Fr elements for pairing points)
**Planned**: 2 limbs per Fq (8 Fr elements for pairing points)

Affects pairing point encoding in final verification step.

## Testing

```bash
cd barretenberg/sol

# Primary test for optimized verifier
forge test --match-path test/honk/blakeOpt.t.sol

# Regenerate after changes
./scripts/sync_blake_opt_vk.sh && ./scripts/copy_optimized_to_cpp.sh -f
```

**Primary test**: `blakeOpt.t.sol` - tests the optimized assembly verifier

**Standard verifier tests** (different code path, for reference only): `Add2`, `Blake`, `ECDSA`, `Recursive` (+ ZK variants)
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,28 @@ template <typename Params> class RecursiveVerifierTest : public testing::Test {
// We expect exactly one connected component (all variables properly connected)
EXPECT_EQ(cc.size(), 1);

// Expected unconstrained variables:
// - MegaBuilder (outer) or ZK flavor: 0
// - UltraBuilder (outer) + non-ZK flavor: 1 (unused Shplonk power from
// compute_shplonk_batching_challenge_powers)
// Expected variables in one gate:
// - Base count of is_infinity booleans (MegaBuilder only, one per deserialized commitment)
// - +1 for unused Shplonk power (non-ZK flavors only)
//
// AUDITTODO: When using MegaBuilder as outer circuit, goblin_element::from_witness() creates
// is_point_at_infinity boolean witnesses for each deserialized commitment. These bools are only
// constrained to be 0/1 (via bool gate) but are not linked to the actual point coordinates.
size_t expected_unconstrained = 0;
if constexpr (!IsMegaBuilder<OuterBuilder> && !RecursiveFlavor::HasZK) {
expected_unconstrained = 1;
if constexpr (IsMegaBuilder<OuterBuilder>) {
// Number of is_infinity booleans depends on number of commitments in the proof
if constexpr (IsAnyOf<RecursiveFlavor,
MegaRecursiveFlavor_<OuterBuilder>,
MegaZKRecursiveFlavor_<OuterBuilder>>) {
expected_unconstrained = 31; // Mega proofs have more commitments
} else {
expected_unconstrained = 28; // Ultra proofs have fewer commitments
}
}
// Add 1 for unused Shplonk power in non-ZK flavors
if constexpr (!RecursiveFlavor::HasZK) {
expected_unconstrained += 1;
}

EXPECT_EQ(variables_in_one_gate.size(), expected_unconstrained);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ void UltraCircuitBuilder_<ExecutionTrace>::fix_witness(const uint32_t witness_in
{
this->assert_valid_variables({ witness_index });

// Mark as intentionally single-gate for boomerang detection
update_used_witnesses(witness_index);

blocks.arithmetic.populate_wires(witness_index, this->zero_idx(), this->zero_idx(), this->zero_idx());
blocks.arithmetic.q_m().emplace_back(0);
blocks.arithmetic.q_1().emplace_back(1);
Expand Down
Loading
Loading