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>;
51
52class BytecodeHashingConstrainingTest : public ::testing::Test {
53 public:
54 EventEmitter<Poseidon2HashEvent> hash_event_emitter;
55 EventEmitter<Poseidon2PermutationEvent> perm_event_emitter;
56 EventEmitter<Poseidon2PermutationMemoryEvent> perm_mem_event_emitter;
57
58 StrictMock<MockGreaterThan> mock_gt;
59 StrictMock<MockExecutionIdManager> mock_execution_id_manager;
60
61 Poseidon2TraceBuilder poseidon2_builder;
62 PrecomputedTraceBuilder precomputed_builder;
63 BytecodeTraceBuilder builder;
64};
65
66class BytecodeHashingConstrainingTestTraceHelper : public BytecodeHashingConstrainingTest {
67 public:
68 TestTraceContainer process_bc_hashing_trace(std::vector<std::vector<FF>> all_bytecode_fields,
69 std::vector<FF> bytecode_ids,
70 std::vector<size_t> bytecode_size_in_bytes)
71 {
72 // Note: this helper expects bytecode fields without the prepended separator and does not complete decomposition
75 TestTraceContainer trace({
76 { { C::precomputed_first_row, 1 } },
77 });
78 uint32_t row = 1;
79 for (uint32_t j = 0; j < all_bytecode_fields.size(); j++) {
80 uint32_t pc_index = 0;
81 auto bytecode_fields = all_bytecode_fields[j];
82 auto bytecode_id = bytecode_ids[j];
83 bytecode_fields.insert(bytecode_fields.begin(),
84 compute_public_bytecode_first_field(bytecode_size_in_bytes[j]));
85 auto hash = poseidon2.hash(bytecode_fields);
86 auto bytecode_field_at = [&bytecode_fields](size_t i) -> FF {
87 return i < bytecode_fields.size() ? bytecode_fields[i] : 0;
88 };
89 auto padding_amount = (3 - (bytecode_fields.size() % 3)) % 3;
90 auto num_rounds = (bytecode_fields.size() + padding_amount) / 3;
91 for (uint32_t i = 0; i < bytecode_fields.size(); i += 3) {
92 bool start = i == 0;
93 bool end = i + 3 >= bytecode_fields.size();
94 auto pc_index_1 = start ? 0 : pc_index + 31;
95 trace.set(row,
96 { {
97 { C::bc_hashing_bytecode_id, bytecode_id },
98 { C::bc_hashing_latch, end },
99 { C::bc_hashing_output_hash, hash },
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_sel_not_start, !start },
113 { C::bc_hashing_start, start },
114 } });
115 if (end) {
116 // Below sets the pc at which the final field starts. We only use/constrain it at latch == 1.
117 FF pc_at_final_field =
118 padding_amount == 2
119 // Two padding fields => we are currently at the final field:
120 ? pc_index
121 // One padding field => the final field starts at pc_index_1
122 // No padding fields => the final field starts at pc_index_2 (= pc_index_1 + 31):
123 : pc_index_1 + (31 * (1 - padding_amount));
124 trace.set(row,
125 { {
126 { C::bc_hashing_pc_at_final_field, pc_at_final_field },
127 } });
128 }
129 row++;
130 num_rounds--;
131 pc_index = pc_index_1 + 62;
132 }
133 }
134 precomputed_builder.process_misc(trace, 256);
135 poseidon2_builder.process_hash(hash_event_emitter.dump_events(), trace);
136 return trace;
137 }
138};
139
140TEST_F(BytecodeHashingConstrainingTest, EmptyRow)
141{
142 check_relation<bc_hashing>(testing::empty_trace());
143}
144
145TEST_F(BytecodeHashingConstrainingTest, SingleBytecodeHashOneRow)
146{
149 std::vector<FF> bytecode_fields = { 1, 2 };
150 std::vector<uint8_t> bytecode = {};
151
152 for (auto bytecode_field : bytecode_fields) {
153 auto bytes = to_buffer(bytecode_field);
154 // Each field elt of encoded bytecode represents 31 bytes, hence start at +1:
155 bytecode.insert(bytecode.end(), bytes.begin() + 1, bytes.end());
156 }
158 auto hash = poseidon2.hash({ sep, 1, 2 });
159
160 auto trace = TestTraceContainer({
161 { { C::precomputed_first_row, 1 } },
162 {
163 { C::bc_hashing_size_in_bytes, bytecode.size() },
164 { C::bc_hashing_input_len, 3 },
165 { C::bc_hashing_latch, 1 },
166 { C::bc_hashing_packed_fields_0, sep },
167 { C::bc_hashing_packed_fields_1, 1 },
168 { C::bc_hashing_packed_fields_2, 2 },
169 { C::bc_hashing_pc_at_final_field, 31 },
170 { C::bc_hashing_pc_index_1, 0 },
171 { C::bc_hashing_pc_index_2, 31 },
172 { C::bc_hashing_sel_not_padding_1, 1 },
173 { C::bc_hashing_sel_not_padding_2, 1 },
174 { C::bc_hashing_bytecode_id, hash },
175 { C::bc_hashing_output_hash, hash },
176 { C::bc_hashing_pc_index, 0 },
177 { C::bc_hashing_rounds_rem, 1 },
178 { C::bc_hashing_sel, 1 },
179 { C::bc_hashing_start, 1 },
180 },
181 });
182
184 builder.process_decomposition(
185 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
187
188 check_relation<bc_hashing>(trace);
189 check_all_interactions<BytecodeTraceBuilder>(trace);
190}
191
192TEST_F(BytecodeHashingConstrainingTestTraceHelper, SingleBytecodeHash100Fields)
193{
194 // The hardcoded value is taken from noir-projects/aztec-nr/aztec/src/hash.nr:
195 FF hash = FF("0x09348974e76c3602893d7a4b4bb52c2ec746f1ade5004ac471d0fbb4587a81a6");
196
197 std::vector<FF> bytecode_fields = {};
198 for (uint32_t i = 1; i < 100; i++) {
199 bytecode_fields.push_back(FF(i));
200 }
201 std::vector<uint8_t> bytecode = {};
202 for (auto bytecode_field : bytecode_fields) {
203 auto bytes = to_buffer(bytecode_field);
204 // Each field elt of encoded bytecode represents 31 bytes, but to_buffer returns 32, hence start at +1:
205 bytecode.insert(bytecode.end(), bytes.begin() + 1, bytes.end());
206 }
207 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode.size() });
208
209 builder.process_decomposition(
210 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
211
212 check_relation<bc_hashing>(trace);
213 check_all_interactions<BytecodeTraceBuilder>(trace);
214 EXPECT_EQ(trace.get(C::bc_hashing_output_hash, 1), hash);
215}
216
217TEST_F(BytecodeHashingConstrainingTestTraceHelper, SingleBytecodeHashMax)
218{
219 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS));
220 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
221 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
222 prepended_fields.insert(prepended_fields.end(), bytecode_fields.begin(), bytecode_fields.end());
223 FF hash = RawPoseidon2::hash(prepended_fields);
224
225 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode.size() });
226 builder.process_decomposition(
227 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
228
229 check_relation<bc_hashing>(trace);
230 check_all_interactions<BytecodeTraceBuilder>(trace);
231}
232
233TEST_F(BytecodeHashingConstrainingTestTraceHelper, MultipleBytecodeHash)
234{
235 // 40 bytes => hash 3 fields, no padding
236 // 20 bytes => hash 2 fields, one padding field
237 // 80 bytes => hash 4 fields, two padding fields
239 std::vector<std::vector<FF>> all_bytecode_fields;
240 std::vector<FF> hashes;
241 std::vector<size_t> byte_sizes;
242 for (uint32_t i = 0; i < all_bytecode.size(); i++) {
243 all_bytecode_fields.push_back(simulation::encode_bytecode(all_bytecode[i]));
244 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(all_bytecode[i].size()) };
245 prepended_fields.insert(prepended_fields.end(), all_bytecode_fields[i].begin(), all_bytecode_fields[i].end());
246 hashes.push_back(RawPoseidon2::hash(prepended_fields));
247 byte_sizes.push_back(all_bytecode[i].size());
248 }
249
250 TestTraceContainer trace = process_bc_hashing_trace(all_bytecode_fields, hashes, byte_sizes);
252
253 for (uint32_t j = 0; j < all_bytecode.size(); j++) {
254 const auto& bytecode = all_bytecode[j];
255 decomp_events.push_back(simulation::BytecodeDecompositionEvent{
256 .bytecode_id = hashes[j], .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) });
257 }
258 builder.process_decomposition({ decomp_events }, trace);
259
260 check_relation<bc_hashing>(trace);
261 check_all_interactions<BytecodeTraceBuilder>(trace);
262}
263
264TEST_F(BytecodeHashingConstrainingTest, BytecodeInteractions)
265{
266 TestTraceContainer trace({
267 { { C::precomputed_first_row, 1 } },
268 });
269
270 std::vector<uint8_t> bytecode = random_bytes(123);
271 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
272 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
273 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
274 FF hash = RawPoseidon2::hash(prepended_fields);
275
276 builder.process_hashing({ { .bytecode_id = hash,
277 .bytecode_length = static_cast<uint32_t>(bytecode.size()),
278 .bytecode_fields = fields } },
279 trace);
280 builder.process_decomposition(
281 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
282
283 tracegen::MultiPermutationBuilder<perm_bc_hashing_get_packed_field_0_settings,
286 perm_builder(C::bc_decomposition_sel_packed);
287 perm_builder.process(trace);
288
289 check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_check_final_bytes_remaining_settings>(trace);
290 check_multipermutation_interaction<BytecodeTraceBuilder,
294 check_relation<bc_hashing>(trace);
295 check_relation<bb::avm2::bc_decomposition<FF>>(trace);
296}
297
298// Negative test where latch == 1 and sel == 0
299TEST_F(BytecodeHashingConstrainingTest, NegativeLatchNotSel)
300{
301 TestTraceContainer trace;
302 trace.set(0,
303 { {
304 { C::bc_hashing_latch, 1 },
305 { C::bc_hashing_sel, 1 },
306 } });
307
308 check_relation<bc_hashing>(trace, bc_hashing::SR_SEL_TOGGLED_AT_LATCH);
309 trace.set(C::bc_hashing_sel, 0, 0); // Mutate to wrong value
310 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_SEL_TOGGLED_AT_LATCH),
311 "SEL_TOGGLED_AT_LATCH");
312}
313
314TEST_F(BytecodeHashingConstrainingTest, NegativeInvalidStartAfterLatch)
315{
316 TestTraceContainer trace({
317 { { C::precomputed_first_row, 1 } },
318 });
319 builder.process_hashing({ { .bytecode_id = 1, .bytecode_length = 62, .bytecode_fields = random_fields(2) },
320 { .bytecode_id = 2, .bytecode_length = 93, .bytecode_fields = random_fields(3) } },
321 trace);
322 check_relation<bc_hashing>(trace, bc_hashing::SR_START_AFTER_LATCH);
323
324 // Row = 2 is the start of the hashing for bytecode id = 2
325 trace.set(Column::bc_hashing_start, 2, 0);
326 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_START_AFTER_LATCH), "START_AFTER_LATCH");
327}
328
329TEST_F(BytecodeHashingConstrainingTest, NegativeInvalidPCIncrement)
330{
331 TestTraceContainer trace({
332 { { C::precomputed_first_row, 1 } },
333 });
334 builder.process_hashing(
335 {
336 { .bytecode_id = 1, .bytecode_length = 124, .bytecode_fields = random_fields(4) },
337 },
338 trace);
339 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS);
340
341 // This is the last row of the bytecode hashing, pc_index should be 62
342 trace.set(Column::bc_hashing_pc_index, 2, 10);
343 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS), "PC_INCREMENTS");
344 // The next pc_index should be 93 = pc_index + 31
345 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_1), "PC_INCREMENTS_1");
346 // The next pc_index should be 124 = pc_index_1 + 31
347 check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_2);
348 trace.set(Column::bc_hashing_pc_index_2, 2, 10);
349 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS_2), "PC_INCREMENTS_2");
350}
351
352TEST_F(BytecodeHashingConstrainingTest, NegativeStartIsSeparator)
353{
354 TestTraceContainer trace({
355 { { C::precomputed_first_row, 1 } },
356 });
357 builder.process_hashing({ { .bytecode_id = 1, .bytecode_length = 62, .bytecode_fields = { 1, 2 } } }, trace);
358 check_relation<bc_hashing>(trace, bc_hashing::SR_START_IS_FIRST_FIELD);
359
360 // Row = 1 is the start of the hashing for bytecode id = 1
361 trace.set(Column::bc_hashing_packed_fields_0, 1, 1);
362 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_START_IS_FIRST_FIELD),
363 "START_IS_FIRST_FIELD");
364}
365
366TEST_F(BytecodeHashingConstrainingTest, NegativeBytecodeInteraction)
367{
368 TestTraceContainer trace({
369 { { C::precomputed_first_row, 1 } },
370 });
371
372 std::vector<uint8_t> bytecode = random_bytes(150);
373 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
374 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
375 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
376 FF hash = RawPoseidon2::hash(prepended_fields);
377
378 builder.process_hashing({ { .bytecode_id = hash, .bytecode_length = 150, .bytecode_fields = fields } }, trace);
379 builder.process_decomposition(
380 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
381 tracegen::MultiPermutationBuilder<perm_bc_hashing_get_packed_field_0_settings,
384 perm_builder(C::bc_decomposition_sel_packed);
385 perm_builder.process(trace);
386
387 // Row = 2 constrains the hashing for the last 3 fields of the bytecode (no padding)
388 // Modify the pc index for the permutation of the first packed field of row 2 (= prepended_fields[3])
389 trace.set(Column::bc_hashing_pc_index, 2, 0);
394 "Failed.*GET_PACKED_FIELD_0. Could not find tuple in destination.");
395 trace.set(Column::bc_hashing_pc_index, 2, 62);
396 // Modify the field value for the permutation of the second packed field of row 2 (= prepended_fields[4])
397 trace.set(Column::bc_hashing_packed_fields_1, 2, 0);
402 "Failed.*GET_PACKED_FIELD_1. Could not find tuple in destination.");
403 trace.set(Column::bc_hashing_packed_fields_1, 2, prepended_fields[4]);
404
405 // Modify the pc index for the permutation of the third packed field of row 2 (= fields[5])
406 trace.set(Column::bc_hashing_pc_index_2, 2, 0);
411 "Failed.*GET_PACKED_FIELD_2. Could not find tuple in destination.");
412
413 // Reset for next test:
414 trace.set(Column::bc_hashing_pc_index_2, 2, 124);
415 check_multipermutation_interaction<BytecodeTraceBuilder,
419
420 // Modify the bytecode id for the permutation:
421 trace.set(Column::bc_hashing_bytecode_id, 2, 0);
426 "Failed.*GET_PACKED_FIELD_.*. Could not find tuple in destination.");
427}
428
429TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingSelectors)
430{
431 // 80 bytes => hash 4 fields, two padding fields
432 std::vector<uint8_t> bytecode = random_bytes(80);
433 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
434
435 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
436 builder.process_decomposition(
437 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
438
439 // Row = 2 constrains the hashing for the last field of the bytecode, plus 2 padding fields
440 // We cannot have padding anywhere but the last hashing row (= latch):
441 trace.set(Column::bc_hashing_latch, 2, 0);
442 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_END), "PADDING_END");
443 trace.set(Column::bc_hashing_latch, 2, 1);
444
445 // We cannot have packed_fields_1 is padding, but packed_fields_2 is not:
446 trace.set(Column::bc_hashing_sel_not_padding_2, 2, 1);
447 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_CONSISTENCY),
448 "PADDING_CONSISTENCY");
449 trace.set(Column::bc_hashing_sel_not_padding_2, 2, 0);
450
451 // We cannot have any padding with non-zero values:
452 trace.set(Column::bc_hashing_packed_fields_1, 2, 1);
453 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDED_BY_ZERO_1), "PADDED_BY_ZERO_1");
454 trace.set(Column::bc_hashing_packed_fields_2, 2, 1);
455 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDED_BY_ZERO_2), "PADDED_BY_ZERO_2");
456}
457
458TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingUnder)
459{
460 // 80 bytes => hash 4 fields, two padding fields
461 std::vector<uint8_t> bytecode = random_bytes(80);
462 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
463 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode.size()) };
464 prepended_fields.insert(prepended_fields.end(), fields.begin(), fields.end());
465 FF hash = RawPoseidon2::hash(prepended_fields);
466
467 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { hash }, { bytecode.size() });
468 builder.process_decomposition(
469 { { .bytecode_id = hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
470
471 // Row = 2 constrains the hashing for the last field of the bytecode, plus 2 padding fields
472 // We cannot claim there is only one padding field:
473 trace.set(Column::bc_hashing_sel_not_padding_1, 2, 1);
474 // This will initially fail, because pc_at_final_field does not correspond to the pc at field 1...
475 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_CORRECTNESS),
476 "PADDING_CORRECTNESS");
477 // ...setting it to that of field 2 will force the relation to pass...
478 trace.set(Column::bc_hashing_pc_at_final_field, 2, 93);
479 check_relation<bc_hashing>(trace, bc_hashing::SR_PADDING_CORRECTNESS);
480 // ...but the lookup to find field 1 will fail...
485 "Failed.*GET_PACKED_FIELD_1. Could not find tuple in destination.");
486 // ...and the lookup to check the final field against bytes remaining will fail:
488 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_check_final_bytes_remaining_settings>(trace)),
489 "Failed.*CHECK_FINAL_BYTES_REMAINING. Could not find tuple in destination.");
490}
491
492TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativePaddingOver)
493{
494 // 100 bytes => hash 5 fields, one padding field
495 std::vector<uint8_t> bytecode = random_bytes(100);
496 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
497
498 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
499 builder.process_decomposition(
500 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
501
502 // Row = 2 constrains the hashing for the last fields of the bytecode, plus 1 padding field
503 // We cannot claim there are two padding fields (to attempt to skip processing the last bytecode field):
504 trace.set(Column::bc_hashing_sel_not_padding_1, 2, 0);
505 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PADDED_BY_ZERO_1), "PADDED_BY_ZERO_1");
506 // If we incorrectly set packed_fields_1 to 0 and pc_at_final_field to pc_index_1...
507 trace.set(Column::bc_hashing_packed_fields_1, 2, 0);
508 trace.set(Column::bc_hashing_pc_at_final_field, 2, 62);
509 // ...then the lookup into decomp will fail (bytes_remaining > 31):
511 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_check_final_bytes_remaining_settings>(trace)),
512 "Failed.*CHECK_FINAL_BYTES_REMAINING. Could not find tuple in destination.");
513}
514
515TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeInputLen)
516{
517 // 80 bytes => hash 4 fields, two padding fields
518 std::vector<uint8_t> bytecode = random_bytes(80);
519 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
520
521 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
522 builder.process_decomposition(
523 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
524
525 // Set the incorrect input_len at the first row, and the lookup into (an honest) poseidon will fail:
526 trace.set(Column::bc_hashing_input_len, 1, 0);
528 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
529 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
530
531 trace.set(Column::bc_hashing_input_len, 1, 4);
532
533 // Set the incorrect input_len at the final row, and the constraining length check will fail:
534 trace.set(Column::bc_hashing_input_len, 2, 0);
535 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS),
536 "BYTECODE_LENGTH_FIELDS");
537}
538
539TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeRounds)
540{
541 // 80 bytes => hash 4 fields, two padding fields
542 std::vector<uint8_t> bytecode = random_bytes(80);
543 std::vector<FF> fields = simulation::encode_bytecode(bytecode);
544
545 TestTraceContainer trace = process_bc_hashing_trace({ fields }, { 1 }, { bytecode.size() });
546 builder.process_decomposition(
547 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
548
549 // Setting the incorrect number of rounds remaining will fail relative to the next row...
550 trace.set(Column::bc_hashing_rounds_rem, 1, 3);
551 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_ROUNDS_DECREMENT), "ROUNDS_DECREMENT");
552
553 // ...and even if decremented correctly, will fail at latch if rounds_rem != 1:
554 trace.set(Column::bc_hashing_rounds_rem, 2, 2);
555 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_ROUNDS_DECREMENT), "ROUNDS_DECREMENT");
556}
557
558TEST_F(BytecodeHashingConstrainingTestTraceHelper, NegativeOutputHash)
559{
560 std::vector<FF> bytecode_fields = random_fields(10);
561 std::vector<FF> prepended_fields = { compute_public_bytecode_first_field(bytecode_fields.size() * 31) };
562 prepended_fields.insert(prepended_fields.end(), bytecode_fields.begin(), bytecode_fields.end());
563 FF hash = RawPoseidon2::hash(prepended_fields);
564 TestTraceContainer trace = process_bc_hashing_trace({ bytecode_fields }, { hash }, { bytecode_fields.size() * 31 });
565
566 check_relation<bc_hashing>(trace);
567 check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace);
568
569 // Change any of the output_hash values
570 trace.set(Column::bc_hashing_output_hash, 2, 123);
572 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
573 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
574}
575
576TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashIncrements)
577{
580 // Attempt to skip some init fields:
581 // decomp: 3 fields 1, 2, 3 => real hash [ sep, 1, 2, 3 ] => try and claim hash [ sep, 2, 3 ] => start = 1, pc_index
582 // = 31. Note that this is protected by the addition of precomputed.first_row in #[PC_INCREMENTS]
583 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * 3));
584 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
585
587
588 auto bad_hash = poseidon2.hash({ sep, bytecode_fields[1], bytecode_fields[2] });
589
590 auto trace = TestTraceContainer({
591 { { C::precomputed_first_row, 1 } },
592 {
593 { C::bc_hashing_latch, 1 },
594 { C::bc_hashing_packed_fields_0, sep },
595 { C::bc_hashing_packed_fields_1, bytecode_fields[1] },
596 { C::bc_hashing_packed_fields_2, bytecode_fields[2] },
597 { C::bc_hashing_pc_at_final_field, 62 },
598 { C::bc_hashing_pc_index_1, 31 },
599 { C::bc_hashing_pc_index_2, 62 },
600 { C::bc_hashing_sel_not_padding_1, 1 },
601 { C::bc_hashing_sel_not_padding_2, 1 },
602 { C::bc_hashing_bytecode_id, 1 },
603 { C::bc_hashing_output_hash, bad_hash },
604 { C::bc_hashing_pc_index, 31 },
605 { C::bc_hashing_sel, 1 },
606 { C::bc_hashing_sel_not_start, 0 },
607 { C::bc_hashing_start, 1 },
608 },
609 });
610
613 builder.process_decomposition(
614 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
615
616 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_PC_INCREMENTS), "PC_INCREMENTS");
617}
618
619TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashLength)
620{
623 // Attempt to prepend fields to the hash
624 // decomp: 3 fields 1, 2, 3 => real hash [ sep, 1, 2, 3 ] => try and claim hash [ a, b, c, sep, 1, 2, 3 ]
625 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * 3));
626 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
627
629
630 auto bad_hash = poseidon2.hash({ 0xa, 0xb, 0xc, sep, bytecode_fields[0], bytecode_fields[1], bytecode_fields[2] });
631
632 auto trace = TestTraceContainer({
633 { { C::precomputed_first_row, 1 } },
634 {
635 { C::bc_hashing_input_len, 7 },
636 { C::bc_hashing_packed_fields_0, sep },
637 { C::bc_hashing_packed_fields_1, bytecode_fields[0] },
638 { C::bc_hashing_packed_fields_2, bytecode_fields[1] },
639 { C::bc_hashing_pc_index_1, 0 },
640 { C::bc_hashing_pc_index_2, 31 },
641 { C::bc_hashing_sel_not_padding_1, 1 },
642 { C::bc_hashing_sel_not_padding_2, 1 },
643 { C::bc_hashing_bytecode_id, 1 },
644 { C::bc_hashing_output_hash, 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_latch, 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_at_final_field, 62 },
657 { C::bc_hashing_pc_index_1, 93 },
658 { C::bc_hashing_pc_index_2, 124 },
659 { C::bc_hashing_bytecode_id, 1 },
660 { C::bc_hashing_output_hash, bad_hash },
661 { C::bc_hashing_pc_index, 62 },
662 { C::bc_hashing_rounds_rem, 1 },
663 { C::bc_hashing_sel, 1 },
664 { C::bc_hashing_sel_not_start, 1 },
665 },
666 });
667
670 builder.process_decomposition(
671 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
672
673 // The correct rows (for input chunks [sep, 1, 2] and [3, 0, 0]) will exist in the poseidon trace, but the start
674 // rows do not line up:
676 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_poseidon2_hash_settings>(trace)),
677 "LOOKUP_BC_HASHING_POSEIDON2_HASH");
678 // At the final row, the length check will fail:
679 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS),
680 "BYTECODE_LENGTH_FIELDS");
681}
682
683TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashLengthBytes)
684{
687 // Attempt to extend the bytecode by zero value bytes (without requiring a new field)
688 // bc: 0xa...f of size 90 => real hash [ (90|sep), 0xa.., .., 0xf000000 ] => try and claim bc = 0xa...f0000 of size
689 // 92 with hash [ (92|sep), 0xa.., .., 0xf000000 ]
690 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(90));
691 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
692
694
695 auto bad_hash = poseidon2.hash({ sep, bytecode_fields[0], bytecode_fields[1], bytecode_fields[2] });
696
697 auto trace = TestTraceContainer({
698 { { C::precomputed_first_row, 1 } },
699 {
700 { C::bc_hashing_size_in_bytes, 92 },
701 { C::bc_hashing_input_len, 4 },
702 { C::bc_hashing_packed_fields_0, sep },
703 { C::bc_hashing_packed_fields_1, bytecode_fields[0] },
704 { C::bc_hashing_packed_fields_2, bytecode_fields[1] },
705 { C::bc_hashing_pc_index_1, 0 },
706 { C::bc_hashing_pc_index_2, 31 },
707 { C::bc_hashing_sel_not_padding_1, 1 },
708 { C::bc_hashing_sel_not_padding_2, 1 },
709 { C::bc_hashing_bytecode_id, 1 },
710 { C::bc_hashing_output_hash, bad_hash },
711 { C::bc_hashing_pc_index, 0 },
712 { C::bc_hashing_rounds_rem, 2 },
713 { C::bc_hashing_sel, 1 },
714 { C::bc_hashing_start, 1 },
715 },
716 {
717 { C::bc_hashing_input_len, 4 },
718 { C::bc_hashing_latch, 1 },
719 { C::bc_hashing_packed_fields_0, bytecode_fields[2] },
720 { C::bc_hashing_packed_fields_1, 0 },
721 { C::bc_hashing_packed_fields_2, 0 },
722 { C::bc_hashing_pc_at_final_field, 62 },
723 { C::bc_hashing_pc_index_1, 93 },
724 { C::bc_hashing_pc_index_2, 124 },
725 { C::bc_hashing_bytecode_id, 1 },
726 { C::bc_hashing_output_hash, bad_hash },
727 { C::bc_hashing_pc_index, 62 },
728 { C::bc_hashing_rounds_rem, 1 },
729 { C::bc_hashing_sel, 1 },
730 { C::bc_hashing_sel_not_start, 1 },
731 },
732 });
733
736 builder.process_decomposition(
737 { { .bytecode_id = 1, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
738
739 // The field length is correct (90 and 92 bytes both require 3 fields, total 4 for sep):
740 check_relation<bc_hashing>(trace, bc_hashing::SR_BYTECODE_LENGTH_FIELDS);
741
742 // The claimed byte length is incorrect:
744 (check_interaction<BytecodeTraceBuilder, perm_bc_hashing_bytecode_length_bytes_settings>(trace)),
745 "PERM_BC_HASHING_BYTECODE_LENGTH_BYTES");
746}
747
748TEST_F(BytecodeHashingConstrainingTest, NegativeSingleBytecodeHashOutputConsistency)
749{
752 // Attempt to prepend fields to the hash
753 // decomp: 5 fields 1, 2, 3, 4, 5 => real hash [ sep, 1, 2, 3, 4, 5 ] => try and claim hash [a, b, c, 3, 4, 5]
754 std::vector<uint8_t> bytecode = random_bytes(static_cast<size_t>(31 * 5));
755 std::vector<FF> bytecode_fields = simulation::encode_bytecode(bytecode);
756
758
759 auto good_hash = poseidon2.hash(
760 { sep, bytecode_fields[0], bytecode_fields[1], bytecode_fields[2], bytecode_fields[3], bytecode_fields[4] });
761 auto bad_hash = poseidon2.hash({ 0xa, 0xb, 0xc, bytecode_fields[2], bytecode_fields[3], bytecode_fields[4] });
762
763 auto trace = TestTraceContainer({
764 { { C::precomputed_first_row, 1 } },
765 {
766 { C::bc_hashing_size_in_bytes, bytecode.size() },
767 { C::bc_hashing_input_len, 6 },
768 { C::bc_hashing_packed_fields_0, sep },
769 { C::bc_hashing_packed_fields_1, bytecode_fields[0] },
770 { C::bc_hashing_packed_fields_2, bytecode_fields[1] },
771 { C::bc_hashing_pc_index_1, 0 },
772 { C::bc_hashing_pc_index_2, 31 },
773 { C::bc_hashing_sel_not_padding_1, 1 },
774 { C::bc_hashing_sel_not_padding_2, 1 },
775 { C::bc_hashing_bytecode_id, good_hash },
776 { C::bc_hashing_output_hash, good_hash },
777 { C::bc_hashing_pc_index, 0 },
778 { C::bc_hashing_rounds_rem, 2 },
779 { C::bc_hashing_sel, 1 },
780 { C::bc_hashing_start, 1 },
781 },
782 {
783 { C::bc_hashing_input_len, 6 },
784 { C::bc_hashing_latch, 1 },
785 { C::bc_hashing_packed_fields_0, bytecode_fields[2] },
786 { C::bc_hashing_packed_fields_1, bytecode_fields[3] },
787 { C::bc_hashing_packed_fields_2, bytecode_fields[4] },
788 { C::bc_hashing_pc_at_final_field, 124 },
789 { C::bc_hashing_pc_index_1, 93 },
790 { C::bc_hashing_pc_index_2, 124 },
791 { C::bc_hashing_sel_not_padding_1, 1 },
792 { C::bc_hashing_sel_not_padding_2, 1 },
793 { C::bc_hashing_bytecode_id, good_hash },
794 { C::bc_hashing_output_hash, bad_hash },
795 { C::bc_hashing_pc_index, 62 },
796 { C::bc_hashing_rounds_rem, 1 },
797 { C::bc_hashing_sel, 1 },
798 { C::bc_hashing_sel_not_start, 1 },
799 },
800 });
803 builder.process_decomposition(
804 { { .bytecode_id = good_hash, .bytecode = std::make_shared<std::vector<uint8_t>>(bytecode) } }, trace);
805
806 // The 'correct' rows (for input chunks [sep, 1, 2] and [3, 4, 5]) will exist in the poseidon trace, so the lookups
807 // will pass...
808 check_all_interactions<BytecodeTraceBuilder>(trace);
809 // ...but the hash check will fail:
810 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_HASH_IS_ID), "HASH_IS_ID");
811 // Changing the id to match will fail the lookup into decomposition, and the propagation check:
812 trace.set(Column::bc_hashing_bytecode_id, 2, bad_hash);
813 check_relation<bc_hashing>(trace, bc_hashing::SR_HASH_IS_ID);
815 (check_interaction<BytecodeTraceBuilder, lookup_bc_hashing_check_final_bytes_remaining_settings>(trace)),
816 "Failed.*CHECK_FINAL_BYTES_REMAINING. Could not find tuple in destination.");
817 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace, bc_hashing::SR_ID_PROPAGATION), "ID_PROPAGATION");
818}
819// =====================================================================
820// Ghost Row Injection Vulnerability Tests
821// =====================================================================
822// These tests verify that ghost rows (sel=0) cannot fire permutations.
823// The fix: sel_not_padding_1 * (1 - sel) = 0 and sel_not_padding_2 * (1 - sel) = 0
824// ensure these selectors are forced to 0 when sel=0.
825
826TEST_F(BytecodeHashingConstrainingTest, NegativeGhostRowInjectionBlocked)
827{
828 // Try to create a ghost row (sel=0) with sel_not_padding_1=1 or sel_not_padding_2=1
829 // which would fire the #[GET_PACKED_FIELD_1] or #[GET_PACKED_FIELD_2] permutations
830 TestTraceContainer trace({
831 { { C::precomputed_first_row, 1 } },
832 {
833 { C::bc_hashing_sel, 0 }, // Ghost row: gadget not active
834 { C::bc_hashing_sel_not_padding_1, 1 }, // Try to fire permutation anyway
835 { C::bc_hashing_sel_not_padding_2, 0 },
836 },
837 });
838
839 // The fix: sel_not_padding_1 * (1 - sel) = 0
840 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace), "SEL_NOT_PADDING_1_REQUIRES_SEL");
841
842 // Reset and try with sel_not_padding_2
843 trace.set(C::bc_hashing_sel_not_padding_1, 1, 0);
844 trace.set(C::bc_hashing_sel_not_padding_2, 1, 1);
845
846 // The fix: sel_not_padding_2 * (1 - sel) = 0
847 EXPECT_THROW_WITH_MESSAGE(check_relation<bc_hashing>(trace), "SEL_NOT_PADDING_2_REQUIRES_SEL");
848}
849
850} // namespace
851} // 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_SEL_TOGGLED_AT_LATCH
static constexpr size_t SR_PADDING_CONSISTENCY
static constexpr size_t SR_ROUNDS_DECREMENT
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_CORRECTNESS
static constexpr size_t SR_HASH_IS_ID
static constexpr size_t SR_ID_PROPAGATION
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)
const FF & get(Column col, uint32_t row) const
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)
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)