Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
rom_ram.test.cpp
Go to the documentation of this file.
1
4#include "ultra_honk.test.hpp"
5
6using namespace bb;
7
8#ifdef STARKNET_GARAGA_FLAVORS
9using FlavorTypes = testing::Types<UltraFlavor,
14 UltraStarknetFlavor,
15 UltraStarknetZKFlavor>;
16#else
18 testing::Types<UltraFlavor, UltraZKFlavor, UltraKeccakFlavor, UltraKeccakZKFlavor, UltraRollupFlavor>;
19#endif
20
21template <typename Flavor> class MemoryTests_ : public UltraHonkTests<Flavor> {
22 public:
23 // helper types to check correctness of memory operations
24 // every time we do a read, we confirm the value is correct by using the corresponding "native" type below.
26 using NativeRamTable = std::vector<fr>;
41 auto& circuit_builder,
42 size_t array_length,
43 size_t num_pair_elts_in_ROM_table = 0, // the number of elements of our ROM table
44 // that will involve _pairs_ of numbers.
45 const size_t read_operations = 0,
46 bool final_arithmetic_gate_and_read = true) // toggles whether we apply a final arithmetic gate and read gate
47 {
48 BB_ASSERT_LTE(num_pair_elts_in_ROM_table,
49 array_length,
50 "cannot set the number of 'pairs of elements to add to the ROM table' to be greater than the "
51 "length of the table");
52 // create a list of random variables, add them to the circuit, and record their witnesses.
53 // these will be the _initial_ elements of the ROM/RAM table. we have one extra to use the set-pair
54 // functionality.
55 std::vector<fr> variables(array_length + 1);
56 std::vector<uint32_t> variable_witnesses(array_length + 1);
57 for (auto [variable, witness] : zip_view(variables, variable_witnesses)) {
58 variable = fr::random_element();
59 witness = circuit_builder.add_variable(variable);
60 }
61
62 // array pointing to the witness indicies whose associated real variable is `i`. this makes testing a bit more
63 // ergonomic.
64 std::vector<uint32_t> index_witness_indices(array_length);
65 for (size_t i = 0; i < array_length; ++i) {
66 index_witness_indices[i] = circuit_builder.put_constant_variable(static_cast<uint64_t>(i));
67 }
68
69 // build our "native" ROM table to check our operations
70 NativeRomTable native_rom_table(array_length);
71 // build out in-circuit ROM table
72 size_t rom_table_id = circuit_builder.create_ROM_array(array_length);
73
74 const size_t num_single_elts_in_ROM_table = array_length - num_pair_elts_in_ROM_table;
75 // set some single ROM elements for the chunk
76 for (size_t i = 0; i < num_single_elts_in_ROM_table; ++i) {
77 circuit_builder.set_ROM_element(rom_table_id, i, variable_witnesses[i]);
78 native_rom_table[i] = std::array{ variables[i], fr::zero() };
79 }
80 // set pairs of ROM values for the second chunk
81 for (size_t i = num_single_elts_in_ROM_table; i < array_length; ++i) {
82 circuit_builder.set_ROM_element_pair(
83 rom_table_id, i, std::array{ variable_witnesses[i], variable_witnesses[i + 1] });
84 native_rom_table[i] = std::array{ variables[i], variables[i + 1] };
85 }
86 // perform some random read operations (which add rows to the execution trace) and check "natively" that the
87 // reads are correct. note that if we are reading a row of the ROM table that had a _pair_ being entered in,
88 // then we _must_ call `read_ROM_array_pair`.
89 for (size_t i = 0; i < read_operations; ++i) {
90 uint32_t random_read_index = static_cast<uint32_t>(
91 engine.get_random_uint32() % array_length); // a random index to read from in my ROM array.
92
93 if (random_read_index < num_single_elts_in_ROM_table) {
94 uint32_t read_witness_index =
95 circuit_builder.read_ROM_array(rom_table_id, index_witness_indices[random_read_index]);
96 // check fidelity of the memory read
97 auto actually_read_value = circuit_builder.get_variable(read_witness_index);
98 auto expected_value = native_rom_table[random_read_index][0];
99 BB_ASSERT_EQ(actually_read_value, expected_value);
100 } else {
101 auto [read_witness_index_1, read_witness_index_2] =
102 circuit_builder.read_ROM_array_pair(rom_table_id, index_witness_indices[random_read_index]);
103 // check fidelity of the pair memory read
104 std::array<fr, 2> actually_read_values = { circuit_builder.get_variable(read_witness_index_1),
105 circuit_builder.get_variable(read_witness_index_2) };
106 auto expected_values = native_rom_table[random_read_index];
107 BB_ASSERT_EQ(actually_read_values[0], expected_values[0]);
108 BB_ASSERT_EQ(actually_read_values[1], expected_values[1]);
109 }
110 }
111 if (final_arithmetic_gate_and_read) {
112 // Final gate checks: construct a `big_add_gate` with random values from the ROM table, then perform another
113 // read (which adds rows to our execution trace). This checks that nothing unexpected happens when we
114 // include basic arithmetic gates.
115
116 // build three random indices, store their witnesses for the final check.
117 // in the case when there are _pairs_ of element in the ROM table row, we only use the _first_ entry for our
118 // gate check.
119 std::array<uint32_t, 3> random_index_witnesses_to_check_computation;
120 std::array<uint32_t, 3> random_indices_to_check_computation;
121 std::array<fr, 3> native_fr_elts_to_check_computation;
122 for (size_t i = 0; i < 3; i++) {
123 uint32_t random_index_to_check_computation =
124 static_cast<uint32_t>(engine.get_random_uint32() % array_length);
125 random_indices_to_check_computation[i] = random_index_to_check_computation;
126 random_index_witnesses_to_check_computation[i] =
127 index_witness_indices[random_index_to_check_computation];
128 native_fr_elts_to_check_computation[i] =
129 native_rom_table[random_index_to_check_computation]
130 [0]; // note that we only use the first entry of
131 // `native_rom_table[random_index_to_check_computation]`.
132 }
133
134 // Perform the reads at the random indices, handling single vs pair reads
135 std::array<uint32_t, 3> final_check_read_witnesses;
136 for (size_t i = 0; i < 3; i++) {
137 const auto random_idx = random_indices_to_check_computation[i];
138 const auto random_idx_witness = random_index_witnesses_to_check_computation[i];
139
140 if (random_idx < num_single_elts_in_ROM_table) {
141 final_check_read_witnesses[i] = circuit_builder.read_ROM_array(rom_table_id, random_idx_witness);
142 } else {
143 // For pairs, we only use the first element in the final check
144 auto [first, _] = circuit_builder.read_ROM_array_pair(rom_table_id, random_idx_witness);
145 final_check_read_witnesses[i] = first;
146 }
147 }
148
149 // add the `big_add_gate`
150 const fr d_value = std::accumulate(
151 native_fr_elts_to_check_computation.begin(), native_fr_elts_to_check_computation.end(), fr::zero());
152 uint32_t d_idx = circuit_builder.add_variable(d_value);
153 circuit_builder.create_big_add_gate({
154 final_check_read_witnesses[0],
155 final_check_read_witnesses[1],
156 final_check_read_witnesses[2],
157 d_idx,
158 1,
159 1,
160 1,
161 -1,
162 0,
163 });
164 // add a read row, to make sure we can intersperse the operations, as expected.
165 uint32_t random_read_index = static_cast<uint32_t>(
167 num_single_elts_in_ROM_table); // a random index to read from in my ROM array. we read from
168 // the part of the table that only has _single_ ROM entries.
169 circuit_builder.read_ROM_array(rom_table_id, index_witness_indices[random_read_index]);
170 }
171 }
172
173 static void build_ROM_table_length_zero(auto& circuit_builder) { circuit_builder.create_ROM_array(0); }
174 static void build_ROM_table_with_uninitialized_values(auto& circuit_builder, size_t array_length)
175 {
176 circuit_builder.create_ROM_array(array_length);
177 }
178 static void build_failing_ROM_table(auto& circuit_builder, size_t array_length, ROMFailureType rom_failure_type)
179 {
181 auto rom_id = circuit_builder.create_ROM_array(array_length);
182 auto zero_idx = circuit_builder.zero_idx();
183 auto random_num = fr::random_element();
184 auto random_variable_idx = circuit_builder.add_variable(random_num);
185 switch (rom_failure_type) {
186 // one element is doubly initialized
188 for (size_t i = 0; i < array_length; ++i) {
189 circuit_builder.set_ROM_element(rom_id, i, zero_idx);
190 }
191 circuit_builder.set_ROM_element(rom_id, engine.get_random_uint32() % array_length, random_variable_idx);
192 break;
193 }
194 // we try to read a single element at a ROM entry that contains a _pair_ of values.
196 for (size_t i = 0; i < array_length; ++i) {
197 circuit_builder.set_ROM_element_pair(rom_id, i, std::array{ random_variable_idx, random_variable_idx });
198 }
199 // read the first element
200 circuit_builder.read_ROM_array(rom_id, zero_idx);
201 break;
202 }
203 };
204 }
205 static void build_random_RAM_table(auto& circuit_builder,
206 size_t array_length,
207 const size_t read_write_operations = 0,
208 bool final_arithmetic_gate_and_read = true)
209 {
210
211 // create a list of random variables, add them to the circuit, and record their witnesses.
212 // these will be the _initial_ elements of the RAM table.
213 std::vector<fr> variables(array_length);
214 std::vector<uint32_t> variable_witnesses(array_length);
215 for (auto [variable, witness] : zip_view(variables, variable_witnesses)) {
216 variable = fr::random_element();
217 witness = circuit_builder.add_variable(variable);
218 }
219
220 // array pointing to the witness indicies whose associated real variable is `i`.
221 // this is used for testing
222 std::vector<uint32_t> index_witness_indices(array_length);
223 for (size_t i = 0; i < array_length; ++i) {
224 index_witness_indices[i] = circuit_builder.put_constant_variable(static_cast<uint64_t>(i));
225 }
226 NativeRamTable native_ram_table(array_length);
227 size_t ram_table_id = circuit_builder.create_RAM_array(array_length);
228 // witness indices of the indicies of the array, as we will have to perform "random write operations"
229 for (size_t i = 0; i < array_length; ++i) {
230 circuit_builder.init_RAM_element(ram_table_id, i, variable_witnesses[i]);
231 native_ram_table[i] = variables[i];
232 }
233
234 // perform some random read and write operations, which add rows to the execution trace.
235 for (size_t i = 0; i < read_write_operations; ++i) {
236 // write ops
237 size_t random_write_index = static_cast<size_t>(engine.get_random_uint32() % array_length);
238 fr random_element = fr::random_element();
239 uint32_t write_variable_witness = circuit_builder.add_variable(random_element);
240 native_ram_table[random_write_index] = random_element;
241 circuit_builder.write_RAM_array(
242 ram_table_id, index_witness_indices[random_write_index], write_variable_witness);
243 // read ops, with a "native" check that the values are correct.
244 size_t random_read_index = static_cast<size_t>(engine.get_random_uint32() % array_length);
245 uint32_t read_witness =
246 circuit_builder.read_RAM_array(ram_table_id, index_witness_indices[random_read_index]);
247 auto read_value = circuit_builder.get_variable(read_witness);
248 auto expected_value = native_ram_table[random_read_index];
249 BB_ASSERT_EQ(read_value, expected_value, "the value the RAM table read was not the expected value");
250 }
251 if (final_arithmetic_gate_and_read) {
252 // Final gate checks: construct a `big_add_gate` with values from the RAM table, then perform another
253 // read (which adds rows to our execution trace). This checks that nothing unexpected happens when we
254 // include basic arithmetic gates.
255
256 // build three random indices, store their witnesses for the final check.
257 std::array<uint32_t, 3> random_index_witnesses_to_check_computation;
258 std::array<fr, 3> native_fr_elts_to_check_computation;
259 for (size_t i = 0; i < 3; i++) {
260 uint32_t random_index_to_check_computation =
261 static_cast<uint32_t>(engine.get_random_uint32() % array_length);
262 random_index_witnesses_to_check_computation[i] =
263 index_witness_indices[random_index_to_check_computation];
264 native_fr_elts_to_check_computation[i] = native_ram_table[random_index_to_check_computation];
265 }
266 // Perform the ops at the random indices, handling single vs pair reads
267 std::array<uint32_t, 3> final_check_read_witnesses;
268 for (size_t i = 0; i < 3; i++) {
269 const auto random_idx_witness = random_index_witnesses_to_check_computation[i];
270 final_check_read_witnesses[i] = circuit_builder.read_RAM_array(ram_table_id, random_idx_witness);
271 }
272
273 // add the `big_add_gate`
274 const fr d_value = std::accumulate(
275 native_fr_elts_to_check_computation.begin(), native_fr_elts_to_check_computation.end(), fr::zero());
276 uint32_t d_idx = circuit_builder.add_variable(d_value);
277 circuit_builder.create_big_add_gate({
278 final_check_read_witnesses[0],
279 final_check_read_witnesses[1],
280 final_check_read_witnesses[2],
281 d_idx,
282 1,
283 1,
284 1,
285 -1,
286 0,
287 });
288 // add a read row, to make sure we can intersperse the operations, as expected.
289 uint32_t random_read_index =
290 engine.get_random_uint32() % array_length; // a random index to read from in my ROM array.
291 circuit_builder.read_RAM_array(ram_table_id, index_witness_indices[random_read_index]);
292 }
293 }
294
295 static void build_RAM_table_length_zero(auto& circuit_builder) { circuit_builder.create_RAM_array(0); }
296};
298
300{
301 using Flavor = TypeParam;
302 using MemoryTests = MemoryTests_<Flavor>;
303 auto circuit_builder = UltraCircuitBuilder();
304 MemoryTests::build_ROM_table_length_zero(circuit_builder);
305
306 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
307 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
308}
310{
311 using Flavor = TypeParam;
312 using MemoryTests = MemoryTests_<Flavor>;
313 auto circuit_builder = UltraCircuitBuilder();
314 size_t array_size = 1;
315 size_t num_pair_elts = 0;
316 size_t num_reads = 0;
317 bool final_arithmetic_gate_and_read = false;
318 MemoryTests::build_random_ROM_table(
319 circuit_builder, array_size, num_pair_elts, num_reads, final_arithmetic_gate_and_read);
320
321 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
322 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
323}
324TYPED_TEST(UltraHonkTests, RomTinyRepeated)
325{
326 using Flavor = TypeParam;
327 using MemoryTests = MemoryTests_<Flavor>;
328 auto circuit_builder = UltraCircuitBuilder();
329 size_t array_size = 2;
330 size_t num_pair_elts = 1;
331 size_t num_reads = 5;
332 // Build multiple ROM tables to test repeated table creation
333 constexpr size_t num_tables = 5;
334 for (size_t i = 0; i < num_tables; ++i) {
335 MemoryTests::build_random_ROM_table(circuit_builder, array_size, num_pair_elts, num_reads);
336 }
337
338 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
339 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
340}
341
343{
344 using Flavor = TypeParam;
345 using MemoryTests = MemoryTests_<Flavor>;
346 auto circuit_builder = UltraCircuitBuilder();
347 MemoryTests::build_RAM_table_length_zero(circuit_builder);
348 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
349 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
350}
352{
353 using Flavor = TypeParam;
354 using MemoryTests = MemoryTests_<Flavor>;
355 auto circuit_builder = UltraCircuitBuilder();
356 MemoryTests::build_RAM_table_length_zero(circuit_builder);
357 size_t array_size = 1;
358 size_t read_write_ops = 5;
359 bool final_arithmetic_gate_and_read = false;
360 MemoryTests::build_random_RAM_table(circuit_builder, array_size, read_write_ops, final_arithmetic_gate_and_read);
361 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
362 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
363}
364
366{
367 using Flavor = TypeParam;
368 using MemoryTests = MemoryTests_<Flavor>;
369 auto circuit_builder = UltraCircuitBuilder();
370 size_t array_size = 15;
371 size_t num_pair_elts = 5;
372 size_t num_reads = 5;
373 size_t read_write_ops = 5;
374 constexpr size_t num_tables = 5;
375 for (size_t i = 0; i < num_tables; ++i) {
376 MemoryTests::build_random_RAM_table(circuit_builder, array_size, read_write_ops);
377 MemoryTests::build_random_ROM_table(circuit_builder, array_size, num_pair_elts, num_reads);
378 }
379 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
380 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
381}
382
383TYPED_TEST(UltraHonkTests, RomFailureDoubleInit)
384{
385 using Flavor = TypeParam;
386 using MemoryTests = MemoryTests_<Flavor>;
387 auto circuit_builder = UltraCircuitBuilder();
388 size_t array_length = 5;
389 auto rom_failure_type = MemoryTests::ROMFailureType::DoubleInit;
390 MemoryTests::build_failing_ROM_table(circuit_builder, array_length, rom_failure_type);
391
392 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
393 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/false);
394}
395
396TYPED_TEST(UltraHonkTests, RomFailureSingleReadAtPair)
397{
398 using Flavor = TypeParam;
399 using MemoryTests = MemoryTests_<Flavor>;
400 auto circuit_builder = UltraCircuitBuilder();
401 size_t array_length = 5;
402 auto rom_failure_type = MemoryTests::ROMFailureType::SingleReadAtPair;
403 MemoryTests::build_failing_ROM_table(circuit_builder, array_length, rom_failure_type);
404
405 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
406 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/false);
407}
408
409// Test malicious initialization value in ROM
410TYPED_TEST(UltraHonkTests, RomMaliciousInitValue)
411{
412 using Flavor = TypeParam;
413 using FF = typename Flavor::FF;
415
416 // Create a simple ROM with one malicious initialization value
417 size_t rom_id = injector.builder.create_ROM_array(5);
418
419 // This witness has value 42 in good proof, 666 in bad proof
420 auto malicious_witness = injector.add_malicious_variable(FF(42), FF(666));
421
422 // Initialize ROM with the malicious witness
423 injector.builder.set_ROM_element(rom_id, 0, malicious_witness);
424
425 // Initialize remaining elements with arbitrary values
426 for (size_t i = 1; i < 5; ++i) {
427 auto good_witness = injector.builder.add_variable(FF::random_element());
428 injector.builder.set_ROM_element(rom_id, i, good_witness);
429 }
430
431 // Read the malicious element to create constraints
432 auto index = injector.builder.put_constant_variable(0);
433 injector.builder.read_ROM_array(rom_id, index);
434
435 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(injector.builder);
436
437 // Run CircuitChecker; expect failure in Memory relation for malicious witness
438 EXPECT_TRUE(CircuitChecker::check(injector.builder)); // good builder passes
439 auto bad_builder = injector.create_builder_with_malicious_witnesses();
440 EXPECT_FALSE(CircuitChecker::check(bad_builder)); // bad builder fails (will print "Failed Memory relation")
441
442 // Run full protocol
443 auto [good_instance, bad_instance] = injector.create_instances();
444 TestFixture::prove_and_verify(good_instance, /*expected_result=*/true);
445 TestFixture::prove_and_verify(bad_instance, /*expected_result=*/false);
446}
447
448// Test malicious witness "out-of-bounds" RAM access
449TYPED_TEST(UltraHonkTests, RamOutOfBoundsRead)
450{
451 using Flavor = TypeParam;
452 using FF = typename Flavor::FF;
454
455 // Create a RAM array of size 5
456 const size_t ram_size = 5;
457 size_t ram_id = injector.builder.create_RAM_array(ram_size);
458
459 // Initialize all elements
460 for (size_t i = 0; i < ram_size; ++i) {
461 auto init_val = injector.builder.add_variable(FF::random_element());
462 injector.builder.init_RAM_element(ram_id, i, init_val);
463 }
464
465 // Create a malicious/invalid index witness:
466 FF good_index = FF(2);
467 FF bad_index = FF(99);
468 auto malicious_index = injector.add_malicious_variable(good_index, bad_index);
469
470 // Create a read using the malicious index
471 auto read_result = injector.builder.read_RAM_array(ram_id, malicious_index);
472
473 // Use the read result in a constraint to ensure it's checked
474 auto expected = injector.builder.add_variable(FF(102)); // value at index 2
475 injector.builder.assert_equal(read_result, expected);
476
477 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(injector.builder);
478
479 // Run CircuitChecker
480 // Expected error: "Failed tag check."
481 EXPECT_TRUE(CircuitChecker::check(injector.builder));
482 auto bad_builder = injector.create_builder_with_malicious_witnesses();
483 EXPECT_FALSE(CircuitChecker::check(bad_builder));
484
485 // Run full protocol
486 auto [good_instance, bad_instance] = injector.create_instances();
487 TestFixture::prove_and_verify(good_instance, /*expected_result=*/true);
488 TestFixture::prove_and_verify(bad_instance, /*expected_result=*/false);
489}
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:77
#define BB_ASSERT_LTE(left, right,...)
Definition assert.hpp:152
#define BB_DISABLE_ASSERTS()
Definition assert.hpp:32
static void build_ROM_table_length_zero(auto &circuit_builder)
static void build_RAM_table_length_zero(auto &circuit_builder)
static void build_failing_ROM_table(auto &circuit_builder, size_t array_length, ROMFailureType rom_failure_type)
std::vector< fr > NativeRamTable
static void build_ROM_table_with_uninitialized_values(auto &circuit_builder, size_t array_length)
std::vector< std::array< fr, 2 > > NativeRomTable
static void build_random_ROM_table(auto &circuit_builder, size_t array_length, size_t num_pair_elts_in_ROM_table=0, const size_t read_operations=0, bool final_arithmetic_gate_and_read=true)
build a random ROM table, together with some read ops and an arithmetic gate. includes several compat...
static void build_random_RAM_table(auto &circuit_builder, size_t array_length, const size_t read_write_operations=0, bool final_arithmetic_gate_and_read=true)
typename Curve::ScalarField FF
Test utility for injecting malicious witness values to test failure modes.
Builder create_builder_with_malicious_witnesses()
Create a copy of the builder with malicious values injected.
std::pair< std::shared_ptr< ProverInstance >, std::shared_ptr< ProverInstance > > create_instances()
Create two prover instances, one based on the good witness values and one based on the malicious valu...
uint32_t add_malicious_variable(const FF &good_val, const FF &bad_val)
Add a "good" variable to the builder and specify a malicious value to inject later.
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
Child class of UltraFlavor that runs with ZK Sumcheck.
virtual uint32_t get_random_uint32()=0
numeric::RNG & engine
testing::Types< MegaFlavor, UltraFlavor, UltraZKFlavor, UltraRollupFlavor > FlavorTypes
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TYPED_TEST_SUITE(ShpleminiTest, TestSettings)
UltraCircuitBuilder_< UltraExecutionTraceBlocks > UltraCircuitBuilder
TYPED_TEST(ShpleminiTest, CorrectnessOfMultivariateClaimBatching)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
static field random_element(numeric::RNG *engine=nullptr) noexcept
static constexpr field zero()