Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
bc_hashing.test.cpp
Go to the documentation of this file.
1#include <gmock/gmock.h>
2#include <gtest/gtest.h>
3
4#include <cstdint>
5#include <memory>
6#include <vector>
7
25
26namespace bb::avm2::constraining {
27namespace {
28
29using ::testing::StrictMock;
30
33
35using simulation::EventEmitter;
36using simulation::MockExecutionIdManager;
37using simulation::MockGreaterThan;
38using simulation::Poseidon2;
39using simulation::Poseidon2HashEvent;
40using simulation::Poseidon2PermutationEvent;
41using simulation::Poseidon2PermutationMemoryEvent;
42using tracegen::BytecodeTraceBuilder;
43using tracegen::Poseidon2TraceBuilder;
44using tracegen::PrecomputedTraceBuilder;
45using tracegen::TestTraceContainer;
46
48using C = Column;
49using bc_hashing = bb::avm2::bc_hashing<FF>;
50using bc_decomposition = bb::avm2::bc_decomposition<FF>;
52
53class BytecodeHashingConstrainingTest : public ::testing::Test {
54 public:
55 EventEmitter<Poseidon2HashEvent> hash_event_emitter;
56 EventEmitter<Poseidon2PermutationEvent> perm_event_emitter;
57 EventEmitter<Poseidon2PermutationMemoryEvent> perm_mem_event_emitter;
58
59 StrictMock<MockGreaterThan> mock_gt;
60 StrictMock<MockExecutionIdManager> mock_execution_id_manager;
61
62 Poseidon2TraceBuilder poseidon2_builder;
63 PrecomputedTraceBuilder precomputed_builder;
64 BytecodeTraceBuilder builder;
65};
66
67class BytecodeHashingConstrainingTestTraceHelper : public BytecodeHashingConstrainingTest {
68 public:
69 TestTraceContainer process_bc_hashing_trace(std::vector<std::vector<FF>> all_bytecode_fields,
70 std::vector<FF> bytecode_ids,
71 std::vector<size_t> bytecode_size_in_bytes)
72 {
73 // Note: this helper expects bytecode fields without the prepended separator and does not complete decomposition
76 TestTraceContainer trace({
77 { { C::precomputed_first_row, 1 } },
78 });
79 uint32_t row = 1;
80 for (uint32_t j = 0; j < all_bytecode_fields.size(); j++) {
81 uint32_t pc_index = 0;
82 auto bytecode_fields = all_bytecode_fields[j];
83 auto bytecode_id = bytecode_ids[j];
84 bytecode_fields.insert(bytecode_fields.begin(),
85 compute_public_bytecode_first_field(bytecode_size_in_bytes[j]));
86 poseidon2.hash(bytecode_fields); // Required to populate Poseidon2 hash events
87 auto bytecode_field_at = [&bytecode_fields](size_t i) -> FF {
88 return i < bytecode_fields.size() ? bytecode_fields[i] : 0;
89 };
90 auto padding_amount = (3 - (bytecode_fields.size() % 3)) % 3;
91 auto num_rounds = (bytecode_fields.size() + padding_amount) / 3;
92 for (uint32_t i = 0; i < bytecode_fields.size(); i += 3) {
93 bool start = i == 0;
94 bool end = i + 3 >= bytecode_fields.size();
95 auto pc_index_1 = start ? 0 : pc_index + 31;
96 trace.set(row,
97 { {
98 { C::bc_hashing_bytecode_id, bytecode_id },
99 { C::bc_hashing_end, end },
100 { C::bc_hashing_size_in_bytes, bytecode_size_in_bytes[j] },
101 { C::bc_hashing_input_len, bytecode_fields.size() },
102 { C::bc_hashing_rounds_rem, num_rounds },
103 { C::bc_hashing_packed_fields_0, bytecode_field_at(i) },
104 { C::bc_hashing_packed_fields_1, bytecode_field_at(i + 1) },
105 { C::bc_hashing_packed_fields_2, bytecode_field_at(i + 2) },
106 { C::bc_hashing_pc_index, pc_index },
107 { C::bc_hashing_pc_index_1, pc_index_1 },
108 { C::bc_hashing_pc_index_2, pc_index_1 + 31 },
109 { C::bc_hashing_sel, 1 },
110 { C::bc_hashing_sel_not_padding_1, end && padding_amount == 2 ? 0 : 1 },
111 { C::bc_hashing_sel_not_padding_2, end && padding_amount > 0 ? 0 : 1 },
112 { C::bc_hashing_padding, padding_amount },
113 { C::bc_hashing_sel_not_start, !start },
114 { C::bc_hashing_start, start },
115 } });
116 row++;
117 num_rounds--;
118 pc_index = pc_index_1 + 62;
119 }
120 }
121 precomputed_builder.process_misc(trace, 256);
122 poseidon2_builder.process_hash(hash_event_emitter.dump_events(), trace);
123 return trace;
124 }
125};
126
127TEST_F(BytecodeHashingConstrainingTest, EmptyRow)
128{
129 check_relation<bc_hashing>(testing::empty_trace());
130}
131
132TEST_F(BytecodeHashingConstrainingTest, SingleBytecodeHashOneRow)
133{
136 std::vector<FF> bytecode_fields = { 1, 2 };
137 std::vector<uint8_t> bytecode = {};
138
139 for (auto bytecode_field : bytecode_fields) {
140 auto bytes = to_buffer(bytecode_field);
141 // Each field elt of encoded bytecode represents 31 bytes, hence start at +1:
142 bytecode.insert(bytecode.end(), bytes.begin() + 1, bytes.end());
143 }
145 auto hash = poseidon2.hash({ sep, 1, 2 });
146
147 auto trace = TestTraceContainer({
148 { { C::precomputed_first_row, 1 } },
149 {
150 { C::bc_hashing_size_in_bytes, bytecode.size() },
151 { C::bc_hashing_input_len, 3 },
152 { C::bc_hashing_end, 1 },
153 { C::bc_hashing_packed_fields_0, sep },
154 { C::bc_hashing_packed_fields_1, 1 },
155 { C::bc_hashing_packed_fields_2, 2 },
156 { C::bc_hashing_pc_index_1, 0 },
157 { C::bc_hashing_pc_index_2, 31 },
158 { C::bc_hashing_sel_not_padding_1, 1 },
159 { C::bc_hashing_sel_not_padding_2, 1 },
160 { C::bc_hashing_bytecode_id, hash },
161 { C::bc_hashing_pc_index, 0 },
162 { C::bc_hashing_rounds_rem, 1 },
163 { C::bc_hashing_sel, 1 },
164 { C::bc_hashing_start, 1 },
165 },
166 });
167
169 builder.process_decomposition(
170 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
172
173 check_relation<bc_hashing>(trace);
174 check_all_interactions<BytecodeTraceBuilder>(trace);
175}
176
177TEST_F(BytecodeHashingConstrainingTestTraceHelper, SingleBytecodeHash100Fields)
178{
179 // The hardcoded value is taken from noir-projects/aztec-nr/aztec/src/hash.nr:
180 FF hash = FF("0x09348974e76c3602893d7a4b4bb52c2ec746f1ade5004ac471d0fbb4587a81a6");
181
182 std::vector<FF> bytecode_fields = {};
183 for (uint32_t i = 1; i < 100; i++) {
184 bytecode_fields.push_back(FF(i));
185 }
186 std::vector<uint8_t> bytecode = {};
187 for (auto bytecode_field : bytecode_fields) {
188 auto bytes = to_buffer(bytecode_field);
189 // Each field elt of encoded bytecode represents 31 bytes, but to_buffer returns 32, hence start at +1:
190 bytecode.insert(bytecode.end(), bytes.begin() + 1, bytes.end());
191 }
192 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode.size() });
193
194 builder.process_decomposition(
195 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
196
197 check_relation<bc_hashing>(trace);
198 check_all_interactions<BytecodeTraceBuilder>(trace);
199}
200
201TEST_F(BytecodeHashingConstrainingTestTraceHelper, SingleBytecodeHashMax)
202{
203 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS));
204 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
205 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
206 prepended_fields.insert(prepended_fields.end(), bytecode_fields.begin(), bytecode_fields.end());
207 FF hash = RawPoseidon2::hash(prepended_fields);
208
209 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode.size() });
210 builder.process_decomposition(
211 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
212
213 check_relation<bc_hashing>(trace);
214 check_all_interactions<BytecodeTraceBuilder>(trace);
215}
216
217TEST_F(BytecodeHashingConstrainingTestTraceHelper, MultipleBytecodeHash)
218{
219 // 40 bytes => hash 3 fields, no padding
220 // 20 bytes => hash 2 fields, one padding field
221 // 80 bytes => hash 4 fields, two padding fields
223 std::vector<std::vector<FF>> all_bytecode_fields;
224 std::vector<FF> hashes;
225 std::vector<size_t> byte_sizes;
226 for (uint32_t i = 0; i < all_bytecode.size(); i++) {
227 all_bytecode_fields.push_back(simulation::encode_bytecode(all_bytecode[i]));
228 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(all_bytecode[i].size()) };
229 prepended_fields.insert(prepended_fields.end(), all_bytecode_fields[i].begin(), all_bytecode_fields[i].end());
230 hashes.push_back(RawPoseidon2::hash(prepended_fields));
231 byte_sizes.push_back(all_bytecode[i].size());
232 }
233
234 TestTraceContainer trace = process_bc_hashing_trace(all_bytecode_fields, hashes, byte_sizes);
236
237 for (uint32_t j = 0; j < all_bytecode.size(); j++) {
238 const auto& bytecode = all_bytecode[j];
239 decomp_events.push_back(simulation::BytecodeDecompositionEvent{
240 .bytecode_id = hashes[j], .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) });
241 }
242 builder.process_decomposition({ decomp_events }, trace);
243
244 check_relation<bc_hashing>(trace);
245 check_all_interactions<BytecodeTraceBuilder>(trace);
246}
247
248TEST_F(BytecodeHashingConstrainingTest, BytecodeInteractions)
249{
250 TestTraceContainer trace({
251 { { C::precomputed_first_row, 1 } },
252 });
253
254 std::vector<uint8_t> bytecode = random_bytes(123);
255 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
256 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
257 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
258 FF hash = RawPoseidon2::hash(prepended_fields);
259
260 builder.process_hashing({ { .bytecode_id = hash,
261 .bytecode_length_in_bytes = static_cast<uint32_t>(bytecode.size()),
262 .bytecode_fields = fields } },
263 trace);
264 builder.process_decomposition(
265 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
266
267 tracegen::MultiPermutationBuilder<perm_bc_hashing_get_packed_field_0_settings,
270 perm_builder(C::bc_decomposition_sel_packed);
271 perm_builder.process(trace);
272
273 check_multipermutation_interaction<BytecodeTraceBuilder,
277 check_relation<bc_hashing>(trace);
278 check_relation<bb::avm2::bc_decomposition<FF>>(trace);
279}
280
281// Negative test where latch == 1 and sel == 0
282TEST_F(BytecodeHashingConstrainingTest, NegativeLatchNotSel)
283{
284 TestTraceContainer trace;
285 trace.set(0,
286 { {
287 { C::bc_hashing_end, 1 },
288 { C::bc_hashing_sel, 1 },
289 } });
290
291 check_relation<bc_hashing>(trace, bc_hashing::SR_SEL_ON_START_OR_END);
292 trace.set(C::bc_hashing_sel, 0, 0); // Mutate to wrong value
293 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_SEL_ON_START_OR_END),
294 "SEL_ON_START_OR_END");
295}
296
297TEST_F(BytecodeHashingConstrainingTest, NegativeInvalidStartAfterLatch)
298{
299 TestTraceContainer trace({
300 { { C::precomputed_first_row, 1 } },
301 });
302 builder.process_hashing(
303 { { .bytecode_id = 1, .bytecode_length_in_bytes = 62, .bytecode_fields = random_fields(2) },
304 { .bytecode_id = 2, .bytecode_length_in_bytes = 93, .bytecode_fields = random_fields(3) } },
305 trace);
306 check_relation<bc_hashing>(trace, bc_hashing::SR_START_AFTER_LATCH);
307
308 // Row = 2 is the start of the hashing for bytecode id = 2
309 trace.set(Column::bc_hashing_start, 2, 0);
310 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_START_AFTER_LATCH), "START_AFTER_LATCH");
311}
312
313TEST_F(BytecodeHashingConstrainingTest, NegativeInvalidPCIncrement)
314{
315 TestTraceContainer trace({
316 { { C::precomputed_first_row, 1 } },
317 });
318 builder.process_hashing(
319 {
320 { .bytecode_id = 1, .bytecode_length_in_bytes = 124, .bytecode_fields = random_fields(4) },
321 },
322 trace);
323 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS);
324 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_1);
325 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_2);
326
327 // This is the last row of the bytecode hashing, pc_index should be 62
328 trace.set(Column::bc_hashing_pc_index, 2, 10);
329 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS), "PC_INCREMENTS");
330 trace.set(Column::bc_hashing_pc_index, 2, 62);
331 trace.set(Column::bc_hashing_pc_index_1, 2, 97);
332 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_1), "PC_INCREMENTS_1");
333 trace.set(Column::bc_hashing_pc_index_1, 2, 93);
334 // The next pc_index should be 124 = pc_index_1 + 31
335 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_2);
336 trace.set(Column::bc_hashing_pc_index_2, 2, 10);
337 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_2), "PC_INCREMENTS_2");
338}
339
340TEST_F(BytecodeHashingConstrainingTest, NegativeStartIsSeparator)
341{
342 TestTraceContainer trace({
343 { { C::precomputed_first_row, 1 } },
344 });
345 builder.process_hashing({ { .bytecode_id = 1, .bytecode_length_in_bytes = 62, .bytecode_fields = { 1, 2 } } },
346 trace);
347 check_relation<bc_hashing>(trace, bc_hashing::SR_START_IS_FIRST_FIELD);
348
349 // Row = 1 is the start of the hashing for bytecode id = 1
350 trace.set(Column::bc_hashing_packed_fields_0, 1, 1);
351 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_START_IS_FIRST_FIELD),
352 "START_IS_FIRST_FIELD");
353}
354
355TEST_F(BytecodeHashingConstrainingTest, NegativeBytecodeInteraction)
356{
357 TestTraceContainer trace({
358 { { C::precomputed_first_row, 1 } },
359 });
360
361 std::vector<uint8_t> bytecode = random_bytes(150);
362 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
363 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
364 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
365 FF hash = RawPoseidon2::hash(prepended_fields);
366
367 builder.process_hashing({ { .bytecode_id = hash, .bytecode_length_in_bytes = 150, .bytecode_fields = fields } },
368 trace);
369 builder.process_decomposition(
370 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
371 tracegen::MultiPermutationBuilder<perm_bc_hashing_get_packed_field_0_settings,
374 perm_builder(C::bc_decomposition_sel_packed);
375 perm_builder.process(trace);
376
377 // Row = 2 constrains the hashing for the last 3 fields of the bytecode (no padding)
378 // Modify the pc index for the permutation of the first packed field of row 2 (= prepended_fields[3])
379 trace.set(Column::bc_hashing_pc_index, 2, 0);
384 "Failed.*GET_PACKED_FIELD_0. Could not find tuple in destination.");
385 trace.set(Column::bc_hashing_pc_index, 2, 62);
386 // Modify the field value for the permutation of the second packed field of row 2 (= prepended_fields[4])
387 trace.set(Column::bc_hashing_packed_fields_1, 2, 0);
392 "Failed.*GET_PACKED_FIELD_1. Could not find tuple in destination.");
393 trace.set(Column::bc_hashing_packed_fields_1, 2, prepended_fields[4]);
394
395 // Modify the pc index for the permutation of the third packed field of row 2 (= fields[5])
396 trace.set(Column::bc_hashing_pc_index_2, 2, 0);
401 "Failed.*GET_PACKED_FIELD_2. Could not find tuple in destination.");
402
403 // Reset for next test:
404 trace.set(Column::bc_hashing_pc_index_2, 2, 124);
405 check_multipermutation_interaction<BytecodeTraceBuilder,
409
410 // Modify the bytecode id for the permutation:
411 trace.set(Column::bc_hashing_bytecode_id, 2, 0);
416 "Failed.*GET_PACKED_FIELD_.*. Could not find tuple in destination.");
417}
418
419TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingSelectors)
420{
421 // 80 bytes => hash 4 fields, two padding fields
422 std::vector<uint8_t> bytecode = random_bytes(80);
423 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
424
425 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
426 builder.process_decomposition(
427 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
428
429 // Row = 2 constrains the hashing for the last field of the bytecode, plus 2 padding fields
430 // We cannot have padding anywhere but the last hashing row (= latch):
431 trace.set(Column::bc_hashing_end, 2, 0);
432 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_END), "PADDING_END");
433 trace.set(Column::bc_hashing_end, 2, 1);
434
435 // We cannot have packed_fields_1 is padding, but packed_fields_2 is not:
436 trace.set(Column::bc_hashing_sel_not_padding_2, 2, 1);
437 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_CONSISTENCY),
438 "PADDING_CONSISTENCY");
439 trace.set(Column::bc_hashing_sel_not_padding_2, 2, 0);
440
441 // We cannot have any padding with non-zero values:
442 trace.set(Column::bc_hashing_packed_fields_1, 2, 1);
443 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDED_BY_ZERO_1), "PADDED_BY_ZERO_1");
444 trace.set(Column::bc_hashing_packed_fields_2, 2, 1);
445 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDED_BY_ZERO_2), "PADDED_BY_ZERO_2");
446}
447
448TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingUnder)
449{
450 // 80 bytes => hash 4 fields, two padding fields
451 std::vector<uint8_t> bytecode = random_bytes(80);
452 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
453 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
454 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
455 FF hash = RawPoseidon2::hash(prepended_fields);
456
457 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { hash }, { bytecode.size() });
458 builder.process_decomposition(
459 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
460
461 // Row = 2 constrains the hashing for the last field of the bytecode, plus 2 padding fields
462 // We cannot claim there is only one padding field:
463 trace.set(Column::bc_hashing_sel_not_padding_1, 2, 1);
464 // This will initially fail, because pc_at_final_field does not correspond to the pc at field 1...
465 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_COMPUTATION),
466 "PADDING_COMPUTATION");
467 // ...setting it to that padding = 1 and input_len = 5 will make all relations to pass...
468 trace.set(Column::bc_hashing_padding, 2, 1);
469 trace.set(Column::bc_hashing_padding, 1, 1);
470 trace.set(Column::bc_hashing_input_len, 1, 5);
471 check_relation<bc_hashing>(trace);
472 // ...but the lookup to find field 1 will fail...
477 "Failed.*GET_PACKED_FIELD_1. Could not find tuple in destination.");
478}
479
480TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingOver)
481{
482 // 100 bytes => hash 5 fields, one padding field
483 std::vector<uint8_t> bytecode = random_bytes(100);
484 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
485 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
486 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
487 FF hash = RawPoseidon2::hash(prepended_fields);
488
489 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { hash }, { bytecode.size() });
490 builder.process_decomposition(
491 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
492
493 // Row = 2 constrains the hashing for the last fields of the bytecode, plus 1 padding field
494 // We cannot claim there are two padding fields (to attempt to skip processing the last bytecode field):
495 trace.set(Column::bc_hashing_sel_not_padding_1, 2, 0);
496 // Padding computation will fail:
497 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_COMPUTATION),
498 "PADDING_COMPUTATION");
499 // If we set padding = 2, fields_1 = 0, and input_len = 4, all relations will pass.
500 trace.set(Column::bc_hashing_padding, 2, 2);
501 trace.set(Column::bc_hashing_padding, 1, 2);
502 trace.set(Column::bc_hashing_packed_fields_1, 2, 0);
503 trace.set(Column::bc_hashing_input_len, 1, 4);
504 check_relation<bc_hashing>(trace);
505
506 // The multipermutation interaction will pass but a multipermutation selector `sel_packed_read[1]` will be
507 // untoggled, which will fail the #[PACKED_ROW_NEEDS_PERM_SELECTOR] (in bc_decomposition.pil) constraint.
508 check_multipermutation_interaction<BytecodeTraceBuilder,
512
514 check_relation<bc_decomposition>(trace, bc_decomposition::SR_PACKED_ROW_NEEDS_PERM_SELECTOR),
515 "PACKED_ROW_NEEDS_PERM_SELECTOR");
516}
517
518TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeInputLen)
519{
520 // 80 bytes => hash 4 fields, two padding fields
521 std::vector<uint8_t> bytecode = random_bytes(80);
522 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
523
524 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
525 builder.process_decomposition(
526 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
527
528 // Set the incorrect input_len at the first row, and the lookup into (an honest) poseidon will fail:
529 trace.set(Column::bc_hashing_input_len, 1, 0);
531 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
532 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
533
534 trace.set(Column::bc_hashing_input_len, 1, 4);
535
536 // Set the incorrect input_len at the start row, and the constraining length check will fail:
537 trace.set(Column::bc_hashing_input_len, 1, 0);
538 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS),
539 "BYTECODE_LENGTH_FIELDS");
540}
541
542TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeRounds)
543{
544 // 80 bytes => hash 4 fields, two padding fields
545 std::vector<uint8_t> bytecode = random_bytes(80);
546 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
547
548 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
549 builder.process_decomposition(
550 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
551
552 // Setting the incorrect number of rounds remaining will fail relative to the next row...
553 trace.set(Column::bc_hashing_rounds_rem, 1, 3);
554 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_ROUNDS_DECREMENT), "ROUNDS_DECREMENT");
555
556 // ...and even if decremented correctly, will fail at latch if rounds_rem != 1:
557 trace.set(Column::bc_hashing_rounds_rem, 2, 2);
558 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_ROUNDS_DECREMENT), "ROUNDS_DECREMENT");
559}
560
561TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeOutputHash)
562{
563 std::vector<FF> bytecode_fields = random_fields(10);
564 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode_fields.size() * 31) };
565 prepended_fields.insert(prepended_fields.end(), bytecode_fields.begin(), bytecode_fields.end());
566 FF hash = RawPoseidon2::hash(prepended_fields);
567 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode_fields.size() * 31 });
568
569 check_relation<bc_hashing>(trace);
570 check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace);
571
572 // Change any of the output_hash values
573 trace.set(Column::bc_hashing_bytecode_id, 2, 123);
575 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
576 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
577}
578
579TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashIncrements)
580{
583 // Attempt to skip some init fields:
584 // decomp: 3 fields 1, 2, 3 => real hash [ sep, 1, 2, 3 ] => try and claim hash [ sep, 2, 3 ] => start = 1,
585 // pc_index = 31. Note that this is protected by the addition of precomputed.first_row in #[PC_INCREMENTS]
586 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * 3));
587 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
588
590
591 auto bad_hash = poseidon2.hash({ sep, bytecode_fields[1], bytecode_fields[2] });
592
593 auto trace = TestTraceContainer({
594 { { C::precomputed_first_row, 1 } },
595 {
596 { C::bc_hashing_end, 1 },
597 { C::bc_hashing_packed_fields_0, sep },
598 { C::bc_hashing_packed_fields_1, bytecode_fields[1] },
599 { C::bc_hashing_packed_fields_2, bytecode_fields[2] },
600 { C::bc_hashing_pc_index_1, 31 },
601 { C::bc_hashing_pc_index_2, 62 },
602 { C::bc_hashing_sel_not_padding_1, 1 },
603 { C::bc_hashing_sel_not_padding_2, 1 },
604 { C::bc_hashing_bytecode_id, bad_hash },
605 { C::bc_hashing_pc_index, 31 },
606 { C::bc_hashing_sel, 1 },
607 { C::bc_hashing_sel_not_start, 0 },
608 { C::bc_hashing_start, 1 },
609 },
610 });
611
614 builder.process_decomposition(
615 { { .bytecode_id = bad_hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
616
617 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS), "PC_INCREMENTS");
618}
619
620TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashLength)
621{
624 // Attempt to prepend fields to the hash
625 // decomp: 3 fields 1, 2, 3 => real hash [ sep, 1, 2, 3 ] => try and claim hash [ a, b, c, sep, 1, 2, 3 ]
626 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * 3));
627 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
628
630
631 auto bad_hash = poseidon2.hash({ 0xa, 0xb, 0xc, sep, bytecode_fields[0], bytecode_fields[1], bytecode_fields[2] });
632
633 auto trace = TestTraceContainer({
634 { { C::precomputed_first_row, 1 } },
635 {
636 { C::bc_hashing_input_len, 7 },
637 { C::bc_hashing_packed_fields_0, sep },
638 { C::bc_hashing_packed_fields_1, bytecode_fields[0] },
639 { C::bc_hashing_packed_fields_2, bytecode_fields[1] },
640 { C::bc_hashing_pc_index_1, 0 },
641 { C::bc_hashing_pc_index_2, 31 },
642 { C::bc_hashing_sel_not_padding_1, 1 },
643 { C::bc_hashing_sel_not_padding_2, 1 },
644 { C::bc_hashing_bytecode_id, bad_hash },
645 { C::bc_hashing_pc_index, 0 },
646 { C::bc_hashing_rounds_rem, 2 },
647 { C::bc_hashing_sel, 1 },
648 { C::bc_hashing_start, 1 },
649 },
650 {
651 { C::bc_hashing_input_len, 7 },
652 { C::bc_hashing_end, 1 },
653 { C::bc_hashing_packed_fields_0, bytecode_fields[2] },
654 { C::bc_hashing_packed_fields_1, 0 },
655 { C::bc_hashing_packed_fields_2, 0 },
656 { C::bc_hashing_pc_index_1, 93 },
657 { C::bc_hashing_pc_index_2, 124 },
658 { C::bc_hashing_bytecode_id, bad_hash },
659 { C::bc_hashing_pc_index, 62 },
660 { C::bc_hashing_rounds_rem, 1 },
661 { C::bc_hashing_sel, 1 },
662 { C::bc_hashing_sel_not_start, 1 },
663 },
664 });
665
668 builder.process_decomposition(
669 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
670
671 // The correct rows (for input chunks [sep, 1, 2] and [3, 0, 0]) will exist in the poseidon trace, but the start
672 // rows do not line up:
674 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
675 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
676 // At the final row, the length check will fail:
677 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS),
678 "BYTECODE_LENGTH_FIELDS");
679}
680
681TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashLengthBytes)
682{
685 // Attempt to extend the bytecode by zero value bytes (without requiring a new field)
686 // bc: 0xa...f of size 90 => real hash [ (90|sep), 0xa.., .., 0xf000000 ] => try and claim bc = 0xa...f0000 of
687 // size 92 with hash [ (92|sep), 0xa.., .., 0xf000000 ]
688 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(90));
689 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
690
692
693 auto bad_hash = poseidon2.hash({ sep, bytecode_fields[0], bytecode_fields[1], bytecode_fields[2] });
694
695 auto trace = TestTraceContainer({
696 { { C::precomputed_first_row, 1 } },
697 {
698 { C::bc_hashing_size_in_bytes, 92 },
699 { C::bc_hashing_input_len, 4 },
700 { C::bc_hashing_packed_fields_0, sep },
701 { C::bc_hashing_packed_fields_1, bytecode_fields[0] },
702 { C::bc_hashing_packed_fields_2, bytecode_fields[1] },
703 { C::bc_hashing_pc_index_1, 0 },
704 { C::bc_hashing_pc_index_2, 31 },
705 { C::bc_hashing_sel_not_padding_1, 1 },
706 { C::bc_hashing_sel_not_padding_2, 1 },
707 { C::bc_hashing_bytecode_id, bad_hash },
708 { C::bc_hashing_pc_index, 0 },
709 { C::bc_hashing_rounds_rem, 2 },
710 { C::bc_hashing_padding, 2 },
711 { C::bc_hashing_sel, 1 },
712 { C::bc_hashing_start, 1 },
713 },
714 {
715 { C::bc_hashing_input_len, 4 },
716 { C::bc_hashing_end, 1 },
717 { C::bc_hashing_packed_fields_0, bytecode_fields[2] },
718 { C::bc_hashing_packed_fields_1, 0 },
719 { C::bc_hashing_packed_fields_2, 0 },
720 { C::bc_hashing_pc_index_1, 93 },
721 { C::bc_hashing_pc_index_2, 124 },
722 { C::bc_hashing_bytecode_id, bad_hash },
723 { C::bc_hashing_pc_index, 62 },
724 { C::bc_hashing_rounds_rem, 1 },
725 { C::bc_hashing_sel, 1 },
726 { C::bc_hashing_sel_not_start, 1 },
727 },
728 });
729
732 builder.process_decomposition(
733 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
734
735 // The field length is correct (90 and 92 bytes both require 3 fields, total 4 for sep):
736 check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS);
737
738 // The claimed byte length is incorrect:
740 (check_interaction<BytecodeTraceBuilder, perm_bc_hashing_bytecode_length_bytes_settings>(trace)),
741 "PERM_BC_HASHING_BYTECODE_LENGTH_BYTES");
742}
743
744// =====================================================================
745// Ghost Row Injection Vulnerability Tests
746// =====================================================================
747// These tests verify that ghost rows (sel=0) cannot fire permutations.
748// The fix: sel_not_padding_1 * (1 - sel) = 0 and sel_not_padding_2 * (1 - sel) = 0
749// ensure these selectors are forced to 0 when sel=0.
750
751TEST_F(BytecodeHashingConstrainingTest, NegativeGhostRowInjectionBlocked)
752{
753 // Try to create a ghost row (sel=0) with sel_not_padding_1=1 or sel_not_padding_2=1
754 // which would fire the #[GET_PACKED_FIELD_1] or #[GET_PACKED_FIELD_2] permutations
755 TestTraceContainer trace({
756 { { C::precomputed_first_row, 1 } },
757 {
758 { C::bc_hashing_sel, 0 }, // Ghost row: gadget not active
759 { C::bc_hashing_sel_not_padding_1, 1 }, // Try to fire permutation anyway
760 { C::bc_hashing_sel_not_padding_2, 0 },
761 },
762 });
763
764 // The fix: sel_not_padding_1 * (1 - sel) = 0
765 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace), "SEL_NOT_PADDING_REQUIRES_SEL");
766
767 // Reset and try with sel_not_padding_2
768 trace.set(C::bc_hashing_sel_not_padding_1, 1, 0);
769 trace.set(C::bc_hashing_sel_not_padding_2, 1, 1);
770
771 // The fix: sel_not_padding_2 * (1 - sel) = 0
772 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace), "SEL_NOT_PADDING_REQUIRES_SEL");
773}
774
775TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingPropagationMultiRow)
776{
777 // 248 bytes => 8 fields + 1 sep = 9 fields => padding = 0, 3 rounds
778 // Verify PADDING_PROPAGATION across a longer computation block.
779 std::vector<uint8_t> bytecode = random_bytes(248);
780 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
781
782 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
783 builder.process_decomposition(
784 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
785
786 check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_PROPAGATION);
787
788 // Rows 1, 2, 3 are the three rounds. Changing padding on the middle row should break propagation.
789 trace.set(C::bc_hashing_padding, 2, 1);
790 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_PROPAGATION),
791 "PADDING_PROPAGATION");
792}
793
794TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeBytecodeFieldLengthViaPadding)
795{
796 // Verify that #[BYTECODE_LENGTH_FIELDS] catches a mismatch between padding and input_len at start.
797 // Constraint: start * (3 * rounds_rem - padding - input_len) = 0
798 // 93 bytes => 3 fields + 1 sep = 4 fields => padding = 2, rounds_rem = 2, input_len = 4
799 // Check: 3 * 2 - 2 - 4 = 0 ✓
800 std::vector<uint8_t> bytecode = random_bytes(93);
801 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
802
803 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
804 builder.process_decomposition(
805 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
806
807 check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS);
808
809 // Corrupt padding at the start row to break the link between rounds_rem, padding, and input_len.
810 // Set padding to 0 at start (should be 2). Now 3 * 2 - 0 - 4 = 2 ≠ 0
811 trace.set(C::bc_hashing_padding, 1, 0);
812 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS),
813 "BYTECODE_LENGTH_FIELDS");
814}
815
816} // namespace
817} // namespace bb::avm2::constraining
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessageRegex)
Definition assert.hpp:193
std::shared_ptr< Napi::ThreadSafeFunction > bytecode
#define MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS
StrictMock< MockGreaterThan > mock_gt
EventEmitter< Poseidon2PermutationMemoryEvent > perm_mem_event_emitter
EventEmitter< Poseidon2PermutationEvent > perm_event_emitter
EventEmitter< Poseidon2HashEvent > hash_event_emitter
Poseidon2TraceBuilder poseidon2_builder
StrictMock< MockExecutionIdManager > mock_execution_id_manager
static constexpr size_t SR_PACKED_ROW_NEEDS_PERM_SELECTOR
static constexpr size_t SR_PADDING_CONSISTENCY
static constexpr size_t SR_PADDING_PROPAGATION
static constexpr size_t SR_ROUNDS_DECREMENT
static constexpr size_t SR_SEL_ON_START_OR_END
static constexpr size_t SR_PADDING_END
static constexpr size_t SR_PC_INCREMENTS_2
static constexpr size_t SR_START_IS_FIRST_FIELD
static constexpr size_t SR_START_AFTER_LATCH
static constexpr size_t SR_PADDED_BY_ZERO_2
static constexpr size_t SR_PADDING_COMPUTATION
static constexpr size_t SR_PADDED_BY_ZERO_1
static constexpr size_t SR_PC_INCREMENTS
static constexpr size_t SR_BYTECODE_LENGTH_FIELDS
static constexpr size_t SR_PC_INCREMENTS_1
void process_hash(const simulation::EventEmitterInterface< simulation::Poseidon2HashEvent >::Container &hash_events, TraceContainer &trace)
Processes the hash events for the Poseidon2 hash function. It populates the columns for the poseidon2...
void process_misc(TraceContainer &trace, const uint32_t num_rows=PRECOMPUTED_TRACE_SIZE)
Populate miscellaneous precomputed columns: first_row selector and idx (row index).
void set(Column col, uint32_t row, const FF &value)
static FF hash(const std::vector< FF > &input)
Hashes a vector of field elements.
PrecomputedTraceBuilder precomputed_builder
Definition alu.test.cpp:120
AluTraceBuilder builder
Definition alu.test.cpp:124
TestTraceContainer trace
void check_multipermutation_interaction(tracegen::TestTraceContainer &trace)
TEST_F(AvmRecursiveTests, TwoLayerAvmRecursionFailsWithWrongPIs)
std::vector< FF > encode_bytecode(std::span< const uint8_t > bytecode)
Encodes the bytecode into a vector of field elements. Each field element represents 31 bytes of the b...
FF compute_public_bytecode_first_field(size_t bytecode_size)
std::vector< uint8_t > random_bytes(size_t n)
Definition fixtures.cpp:33
TestTraceContainer empty_trace()
Definition fixtures.cpp:153
std::vector< FF > random_fields(size_t n)
Definition fixtures.cpp:23
AvmFlavorSettings::FF FF
Definition field.hpp:10
permutation_settings< perm_bc_hashing_get_packed_field_2_settings_ > perm_bc_hashing_get_packed_field_2_settings
permutation_settings< perm_bc_hashing_get_packed_field_1_settings_ > perm_bc_hashing_get_packed_field_1_settings
permutation_settings< perm_bc_hashing_get_packed_field_0_settings_ > perm_bc_hashing_get_packed_field_0_settings
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::vector< uint8_t > to_buffer(T const &value)