Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ultra_circuit_builder_lookup.test.cpp
Go to the documentation of this file.
4
5#include <gtest/gtest.h>
6#include <unordered_map>
7#include <unordered_set>
8
9using namespace bb;
10
11class UltraCircuitBuilderLookup : public ::testing::Test {
12 protected:
15};
16
17// Verifies that a valid lookup operation creates the expected number of gates and passes circuit check
19{
21
22 // UINT32_XOR decomposes into 6 lookups: five 6-bit tables, one 2-bit table
23 const fr a_value(42);
24 const fr b_value(17);
25 const auto a_idx = builder.add_variable(a_value);
26 const auto b_idx = builder.add_variable(b_value);
27
28 const auto accumulators =
29 plookup::get_lookup_accumulators(plookup::MultiTableId::UINT32_XOR, a_value, b_value, true);
30 const auto result =
31 builder.create_gates_from_plookup_accumulators(plookup::MultiTableId::UINT32_XOR, accumulators, a_idx, b_idx);
32
33 // First lookup should reuse input indices
34 EXPECT_EQ(result[ColumnIdx::C1][0], a_idx);
35 EXPECT_EQ(result[ColumnIdx::C2][0], b_idx);
36
37 // Check builder state
38 EXPECT_EQ(result[ColumnIdx::C1].size(), 6UL);
39 EXPECT_EQ(result[ColumnIdx::C2].size(), 6UL);
40 EXPECT_EQ(result[ColumnIdx::C3].size(), 6UL);
41 EXPECT_EQ(builder.blocks.lookup.size(), 6UL);
42
43 // Check circuit satisfaction
44 EXPECT_TRUE(CircuitChecker::check(builder));
45}
46
47// Verifies that step size coefficients are set correctly for each gate in a multi-table lookup
48TEST_F(UltraCircuitBuilderLookup, StepSizeCoefficients)
49{
51
52 const fr a_value(7);
53 const fr b_value(14);
54 const auto a_idx = builder.add_variable(a_value);
55 const auto b_idx = builder.add_variable(b_value);
56
57 const auto accumulators =
58 plookup::get_lookup_accumulators(plookup::MultiTableId::UINT32_XOR, a_value, b_value, true);
59 builder.create_gates_from_plookup_accumulators(plookup::MultiTableId::UINT32_XOR, accumulators, a_idx, b_idx);
60
61 const auto& multi_table = plookup::get_multitable(plookup::MultiTableId::UINT32_XOR);
62 const size_t num_lookups = multi_table.column_1_step_sizes.size();
63
64 // Check that step sizes have been populated correctly in the the corresponding selectors
65 for (size_t i = 0; i < num_lookups - 1; ++i) {
66 EXPECT_EQ(builder.blocks.lookup.q_2()[i], -multi_table.column_1_step_sizes[i + 1]);
67 EXPECT_EQ(builder.blocks.lookup.q_m()[i], -multi_table.column_2_step_sizes[i + 1]);
68 EXPECT_EQ(builder.blocks.lookup.q_c()[i], -multi_table.column_3_step_sizes[i + 1]);
69 }
70
71 // Check last gate has zero step sizes
72 const size_t last_idx = num_lookups - 1;
73 EXPECT_EQ(builder.blocks.lookup.q_2()[last_idx], fr(0));
74 EXPECT_EQ(builder.blocks.lookup.q_m()[last_idx], fr(0));
75 EXPECT_EQ(builder.blocks.lookup.q_c()[last_idx], fr(0));
76
77 // Check that remaining selectors are set correctly
78 for (size_t i = 0; i < num_lookups; ++i) {
79 const auto& table = builder.get_table(multi_table.basic_table_ids[i]);
80 EXPECT_EQ(builder.blocks.lookup.q_3()[i], fr(table.table_index)); // unique table identifier
81 EXPECT_EQ(builder.blocks.lookup.gate_selector_for(bb::GateKind::Lookup)[i],
82 fr(1)); // gate selector should be "on"
83 EXPECT_EQ(builder.blocks.lookup.q_1()[i], fr(0)); // unused in lookup gates
84 EXPECT_EQ(builder.blocks.lookup.q_4()[i], fr(0)); // unused in lookup gates
85 }
86
87 EXPECT_TRUE(CircuitChecker::check(builder));
88}
89
90// Verifies that different tables get unique indices
91TEST_F(UltraCircuitBuilderLookup, DifferentTablesGetUniqueIndices)
92{
94
95 // Specify three different table IDs
96 const auto table_id1 = plookup::BasicTableId::UINT_XOR_SLICE_6_ROTATE_0;
97 const auto table_id2 = plookup::BasicTableId::UINT_XOR_SLICE_2_ROTATE_0;
98 const auto table_id3 = plookup::BasicTableId::UINT_AND_SLICE_6_ROTATE_0;
99
100 // Construct four tables, three unique and one duplicate
101 auto& table1 = builder.get_table(table_id1);
102 auto& table2 = builder.get_table(table_id2);
103 auto& table1_again = builder.get_table(table_id1); // duplicate of table1
104 auto& table3 = builder.get_table(table_id3);
105
106 // table1 and table1_again should be the same reference
107 EXPECT_EQ(&table1, &table1_again);
108
109 // Table IDs should be set correctly
110 EXPECT_EQ(table1.id, table_id1);
111 EXPECT_EQ(table2.id, table_id2);
112 EXPECT_EQ(table1_again.id, table_id1);
113 EXPECT_EQ(table3.id, table_id3);
114
115 // Tables should have `table_index` based on order of creation, starting from 1 (0 is reserved).
116 EXPECT_EQ(table1.table_index, 1UL);
117 EXPECT_EQ(table2.table_index, 2UL);
118 EXPECT_EQ(table1_again.table_index, 1UL);
119 EXPECT_EQ(table3.table_index, 3UL);
120
121 // Exactly three different tables should have been created
122 EXPECT_EQ(builder.get_num_lookup_tables(), 3UL);
123}
124
125// Every basic table reachable through any MultiTable must receive a unique, positive circuit-local
126// table_index. The LogDeriv lookup relation identifies a table solely by table_index (not BasicTableId),
127// so a generator that stores anything other than the builder-assigned index silently collapses distinct tables to one
128// identity. This sweeps the whole table space so any such generator is caught.
129TEST_F(UltraCircuitBuilderLookup, AllMultiTableBasicTablesGetUniquePositiveIndices)
130{
132 for (size_t mt = 0; mt < static_cast<size_t>(plookup::MultiTableId::NUM_MULTI_TABLES); ++mt) {
133 const auto& multitable = plookup::get_multitable(static_cast<plookup::MultiTableId>(mt));
134 for (const auto id : multitable.basic_table_ids) {
135 builder.get_table(id);
136 }
137 }
138
139 std::unordered_set<size_t> seen;
140 for (const auto& table : builder.get_lookup_tables()) {
141 EXPECT_GT(table.table_index, 0UL) << "non-positive index for basic table id " << static_cast<size_t>(table.id);
142 EXPECT_TRUE(seen.insert(table.table_index).second)
143 << "duplicate table_index " << table.table_index << " for basic table id " << static_cast<size_t>(table.id);
144 }
145 EXPECT_NO_THROW(builder.finalize_circuit());
146}
147
148TEST_F(UltraCircuitBuilderLookup, FinalizationRejectsDuplicateTableIndices)
149{
151 builder.get_table(plookup::BasicTableId::UINT_XOR_SLICE_6_ROTATE_0);
152 builder.get_table(plookup::BasicTableId::UINT_AND_SLICE_6_ROTATE_0);
153 builder.get_lookup_tables()[1].table_index = builder.get_lookup_tables()[0].table_index;
154
155 EXPECT_THROW_OR_ABORT(builder.finalize_circuit(), "Lookup table indices must be unique within a circuit");
156}
157
158TEST_F(UltraCircuitBuilderLookup, FinalizationRejectsZeroTableIndex)
159{
161 builder.get_table(plookup::BasicTableId::UINT_XOR_SLICE_6_ROTATE_0).table_index = 0;
162
163 EXPECT_THROW_OR_ABORT(builder.finalize_circuit(), "Lookup table indices must be positive");
164}
165
166// Verifies correct behavior when key_b_index is not provided (2-to-1 lookup without second index)
168{
170
171 // HONK_DUMMY_MULTI is a 2-to-1 lookup (two keys, one result)
172 // Tables only contain entries for values 0 and 1 (base = 1 << 1)
173 const fr a_value(1);
174 const fr b_value(0);
175 const auto a_idx = builder.add_variable(a_value);
176 // Not providing b_idx - it will be created from accumulators
177
178 const auto accumulators =
179 plookup::get_lookup_accumulators(plookup::MultiTableId::HONK_DUMMY_MULTI, a_value, b_value, true);
180 const auto result = builder.create_gates_from_plookup_accumulators(
181 plookup::MultiTableId::HONK_DUMMY_MULTI, accumulators, a_idx, std::nullopt);
182
183 // First lookup should reuse a_idx for C1
184 EXPECT_EQ(result[ColumnIdx::C1][0], a_idx);
185
186 // C2 and C3 should be newly created variables
187 EXPECT_NE(result[ColumnIdx::C2][0], a_idx);
188 EXPECT_NE(result[ColumnIdx::C3][0], a_idx);
189
190 EXPECT_TRUE(CircuitChecker::check(builder));
191}
192
193// Verifies that lookup entries are recorded in the table's lookup_gates vector
194TEST_F(UltraCircuitBuilderLookup, LookupEntriesRecorded)
195{
197
198 const fr a_value(33);
199 const fr b_value(44);
200 const auto a_idx = builder.add_variable(a_value);
201 const auto b_idx = builder.add_variable(b_value);
202
203 const auto accumulators =
204 plookup::get_lookup_accumulators(plookup::MultiTableId::UINT32_XOR, a_value, b_value, true);
205
206 const auto& multi_table = plookup::get_multitable(plookup::MultiTableId::UINT32_XOR);
207
208 // Get unique table IDs and record their initial sizes
209 // Note: UINT32_XOR uses UINT_XOR_SLICE_6_ROTATE_0 five times and UINT_XOR_SLICE_2_ROTATE_0 once
212
213 for (const auto& table_id : multi_table.basic_table_ids) {
214 if (initial_sizes.find(table_id) == initial_sizes.end()) {
215 auto& table = builder.get_table(table_id);
216 initial_sizes[table_id] = table.lookup_gates.size();
217 expected_additions[table_id] = 0;
218 }
219 expected_additions[table_id]++;
220 }
221
222 builder.create_gates_from_plookup_accumulators(plookup::MultiTableId::UINT32_XOR, accumulators, a_idx, b_idx);
223
224 // Check that each unique table received the correct number of new lookup entries
225 for (const auto& [table_id, initial_size] : initial_sizes) {
226 auto& table = builder.get_table(table_id);
227 EXPECT_EQ(table.lookup_gates.size(), initial_size + expected_additions[table_id]);
228 }
229
230 EXPECT_TRUE(CircuitChecker::check(builder));
231}
232
233// Verifies that corrupting any accumulator position in any column causes circuit check to fail
234TEST_F(UltraCircuitBuilderLookup, BadAccumulatorFaiure)
235{
236 auto test_corrupt_accumulator = [](ColumnIdx column, size_t position) {
238
239 const fr a_value(123);
240 const fr b_value(456);
241 const auto a_idx = builder.add_variable(a_value);
242 const auto b_idx = builder.add_variable(b_value);
243
244 // Get valid accumulators
245 auto accumulators = plookup::get_lookup_accumulators(plookup::MultiTableId::UINT32_XOR, a_value, b_value, true);
246
247 // Corrupt the specified accumulator entry
248 accumulators[column][position] += fr(1);
249
250 builder.create_gates_from_plookup_accumulators(plookup::MultiTableId::UINT32_XOR, accumulators, a_idx, b_idx);
251
252 // Circuit should fail because the corrupted accumulator doesn't match the table
253 EXPECT_FALSE(CircuitChecker::check(builder));
254 };
255
256 // UINT32_XOR has 6 lookups (five 6-bit tables, one 2-bit table)
257 const size_t num_lookups = 6;
258
259 // Test corrupting each position in each column
260 for (size_t i = 0; i < num_lookups; ++i) {
261 // Note: C1[0] and C2[0] are not tested because the first lookup gate reuses the existing
262 // witness indices (key_a_index and key_b_index) rather than creating new witnesses from
263 // accumulators[C1][0] and accumulators[C2][0]
264 if (i > 0) {
265 test_corrupt_accumulator(ColumnIdx::C1, i);
266 test_corrupt_accumulator(ColumnIdx::C2, i);
267 }
268 // C3 is always created from accumulators, so test all positions
269 test_corrupt_accumulator(ColumnIdx::C3, i);
270 }
271}
272
273// Verifies that invalid input witness values (C1[0] and C2[0]) cause circuit check to fail
274TEST_F(UltraCircuitBuilderLookup, InvalidInputWitnessFailure)
275{
276 const fr a_value(123);
277 const fr b_value(456);
278
279 // Compute accumulators based on the genuine values
280 const auto accumulators =
281 plookup::get_lookup_accumulators(plookup::MultiTableId::UINT32_XOR, a_value, b_value, true);
282
283 // Test with wrong witness value for key_a (first input, reused as C1[0])
284 {
286
287 // Create witness with bad value for first input
288 const fr bad_a_value(666);
289 const auto bad_a_idx = builder.add_variable(bad_a_value);
290 const auto b_idx = builder.add_variable(b_value);
291
292 builder.create_gates_from_plookup_accumulators(
293 plookup::MultiTableId::UINT32_XOR, accumulators, bad_a_idx, b_idx);
294
295 // Circuit should fail because witness at a_idx doesn't match what accumulators expect
296 EXPECT_FALSE(CircuitChecker::check(builder));
297 }
298
299 // Test with wrong witness value for key_b (second input, reused as C2[0])
300 {
302
303 // Create witness with bad value for second input
304 const fr bad_b_value(666);
305 const auto a_idx = builder.add_variable(a_value);
306 const auto bad_b_idx = builder.add_variable(bad_b_value);
307
308 builder.create_gates_from_plookup_accumulators(
309 plookup::MultiTableId::UINT32_XOR, accumulators, a_idx, bad_b_idx);
310
311 // Circuit should fail because witness at b_idx doesn't match what accumulators expect
312 EXPECT_FALSE(CircuitChecker::check(builder));
313 }
314}
#define EXPECT_THROW_OR_ABORT(statement, matcher)
Definition assert.hpp:223
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
AluTraceBuilder builder
Definition alu.test.cpp:124
ReadData< bb::fr > get_lookup_accumulators(const MultiTableId id, const fr &key_a, const fr &key_b, const bool is_2_to_1_lookup)
Given a table ID and the key(s) for a key-value lookup, return the lookup accumulators.
const MultiTable & get_multitable(const MultiTableId id)
Return the multitable with the provided ID; construct all MultiTables if not constructed already.
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
field< Bn254FrParams > fr
Definition fr.hpp:155
UltraCircuitBuilder_< UltraExecutionTraceBlocks > UltraCircuitBuilder
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
TEST_F(UltraCircuitBuilderLookup, BasicLookup)
VectorField result