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