Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
op_decomposition.test.cpp
Go to the documentation of this file.
1#include <gmock/gmock.h>
2#include <gtest/gtest.h>
3
4#include <cstdint>
5
7
8/******************************************************************************
9 CODE GENERATION OF OPERAND DECOMPOSITION INTO BYTES
10*******************************************************************************
11This test serves the purpose of code-generate the operand decomposition into bytes
12which can be statically derived by the wire format of the opcodes specified in
13the map WireOpCode_WIRE_FORMAT (see serialization.hpp).
14
15The artifacts are generated by writing to the standard output:
16- Precomputed Selectors Table which must be copied to: WireOpCode_DC_SELECTORS in instruction_spec.cpp
17- PIL code with relations copied to: instr_fetching.pil
18
19The precomputed selectors table consists of a row per wire opcode and as many
20columns as selector (determined by the algorithm below).
21Example row: { WireOpCode::SHR_16, { 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }
22
23The pil relations map a fixed size bytecode chunk starting from a wire opcode byte
24into the operands of the opcode as defined by WireOpCode_WIRE_FORMAT.
25Example: op4 = sel_op_dc_0 * (bd9 * 2**8 + bd10 * 2**0) + sel_op_dc_4 * (bd8 * 2**8 + bd9 * 2**0) + sel_op_dc_7 * (bd8 *
262**0);
27
28The bytecode chunk consists of the opcode, addressing mode, and 5 operands.
29Bytes of bytecode are denoted: bd0, bd1, bd2, bd3, ...., bd37 where bd0 is the
30opcode and never appears in the above pil relation as it is trivially not an operand.
31Hence we deal with 6 values: addressing_mode, op1, op2, op3, op4, op5
32We refer to these 6 values, including addressing_mode, as 'operands' from now on for ease of reading.
33
34The goal of the algorithm below is to determine the least number of selectors sel_op_dc_XXX
35required to decompose each operand into bytes covering all opcode formats.
36Each selector sel_op_dc_XXX determines a subset of opcodes as defined per the table
37WireOpCode_DC_SELECTORS. For any given column, select all opcodes for which the
38entry bit is toggled.
39
40Below, we denote the 6 operands [addressing_mode, op1, ..., op5]. An operand layout is a
41pair (offset, len) which corresponds to a byte offset of the wire format and len being the
42length in bytes.
43
44Example CAST_8: { OperandType::INDIRECT8, OperandType::UINT8, OperandType::UINT8, OperandType::TAG }
45[{0,1}, {1,1}, {2,1}, {3,1}]
46
47Example SET_128: { OperandType::INDIRECT8, OperandType::UINT16, OperandType::TAG, OperandType::UINT128 }
48[{0,1}, {1,2}, {3,1}, {4,16}]
49
50The algorithm consists in processing WireOpCode_WIRE_FORMAT and map every combination
51[addressing_mode/op{idx}, op_layout] to a subset of opcodes. Continuing the example above,
52the SET_128 opcode would be present in the subsets indexed by the following triples:
53[addressing_mode, {0,1}]
54[op1, {1,2}]
55[op2, {3,1}]
56[op3, {4,16}]
57
58Each subset is then encoded into a bitmask of opcodes packed as a uint128_t key.
59Each unique subset is then mapped to a selector sel_op_dc_XXX.
60
61Assuming in the example above that [op1, {1,2}] maps to selector sel_op_dc_13, the
62corresponding PIL relation will be of the form:
63op1 = sel_op_dc_13 * (bd2 * 2**8 + bd3 * 2**0) + sel_op_dc_XXX * (...) + sel_op_dc_XXX * () ...
64Note that {offset, len} = {1, 2} means that offset is bd2 (bd0 is the opcode byte and
65therefore requires increasing the offset by one) and the length is two bytes, therefore
66bd2 and bd3 are the bytes encoding (in big endian) operand op1.
67
68In addition, we add a naive algorithm to remove a partitioned subset by two other
69subsets and aliases the corresponding selector of the partitioned subset as
70an addition of the selectors pertaining to the 2 other subsets.
71For instance: pol sel_op_dc_18 = sel_op_dc_1 + sel_op_dc_6;
72This allows to save some selectors.
73
74******************************************************************************/
75
76namespace bb::avm2::constraining {
78namespace {
79
80constexpr std::string OPERAND_PREFIX = "op";
81constexpr std::string BYTE_PREFIX = "bd";
82constexpr std::string SELECTOR_PREFIX = "sel_op_dc_";
83
84constexpr size_t NUM_OF_OPERANDS = 6; // Need 5 = AVM_MAX_OPERANDS to cover ECADD, +1 for addressing_mode
85
86struct OperandLayout {
87 uint8_t offset = 0;
88 uint8_t len = 0;
89};
90
91struct Partition {
95};
96
97uint32_t encode_operand_idx_with_layout(uint8_t operand_idx, uint8_t offset, uint8_t len)
98{
99 uint32_t layout = len;
100 layout += (static_cast<uint32_t>(offset) << 8);
101 layout += (static_cast<uint32_t>(operand_idx) << 16);
102 return layout;
103}
104
105uint8_t get_op_idx(uint32_t op_idx_with_layout)
106{
107 return static_cast<uint8_t>(op_idx_with_layout >> 16);
108}
109
110OperandLayout get_op_layout(uint32_t op_idx_with_layout)
111{
112 uint8_t offset = static_cast<uint8_t>((op_idx_with_layout >> 8) & 0xFF);
113 uint8_t len = static_cast<uint8_t>(op_idx_with_layout & 0xFF);
114 return OperandLayout{ .offset = offset, .len = len };
115}
116
117uint128_t encode_subset_wire_opcodes(const std::unordered_set<WireOpCode>& set)
118{
119 uint128_t value = 0;
120 for (const auto& wire_opcode : set) {
121 value += (static_cast<uint128_t>(1) << static_cast<uint8_t>(wire_opcode));
122 }
123
124 return value;
125}
126
127std::string render_selector_array(WireOpCode wire_opcode, const std::vector<uint128_t>& sel_bitmasks)
128{
129 size_t num_of_selectors = sel_bitmasks.size();
130 std::vector<bool> selectors;
131 selectors.reserve(num_of_selectors);
132
133 for (const auto& bitmask : sel_bitmasks) {
134 selectors.push_back(((static_cast<uint128_t>(1) << static_cast<uint128_t>(wire_opcode)) & bitmask) != 0);
135 }
136
137 std::string output = format("{", selectors[0]);
138 for (size_t i = 1; i < num_of_selectors; i++) {
139 output += format(", ", selectors[i]);
140 }
141 output += "}";
142 return output;
143}
144
145auto add_fold = [](const std::string& a, const std::string& b) { return a + " + " + b; };
146
147// Write pol sel_i = sel_j + sel_k
148std::string render_partitions_pil(const std::vector<Partition>& partitions,
149 const std::unordered_map<uint128_t, size_t>& bitmask_to_sel_idx)
150{
151 std::string output;
152 for (const auto& partition : partitions) {
153 output += format("pol ", SELECTOR_PREFIX, bitmask_to_sel_idx.at(partition.union_subset));
154 output += format(" = ", SELECTOR_PREFIX, bitmask_to_sel_idx.at(partition.subset_1));
155 output += format(" + ", SELECTOR_PREFIX, bitmask_to_sel_idx.at(partition.subset_2), ";\n");
156 }
157 return output;
158}
159
160// Output a string like: bd4 * 2**24 + bd5 * 2**16 + bd6 * 2**8 + bd7 * 2**0
161std::string render_operand_layout_pil(OperandLayout layout)
162{
163 std::vector<std::string> monomials;
164 monomials.reserve(layout.len);
165 uint8_t byte_offset = layout.offset;
166 for (int i = 0; i < layout.len; i++) {
167 monomials.push_back(
168 format(BYTE_PREFIX, byte_offset + i + 1, " * 2**", 8 * (layout.len - i - 1))); // Big-endian bytes
169 }
170
171 return std::accumulate(std::next(monomials.begin()), monomials.end(), monomials[0], add_fold);
172}
173
174std::string render_pil(
175 const std::array<std::vector<std::pair<size_t, OperandLayout>>, NUM_OF_OPERANDS>& sel_layout_breakdowns)
176{
177 std::string pil_equations;
178 for (uint8_t i = 0; i < NUM_OF_OPERANDS; i++) {
179 pil_equations += (i == 0) ? "#[ADDRESSING_MODE_BYTES_DECOMPOSITION]\n"
180 : format("#[OP", static_cast<uint32_t>(i), "_BYTES_DECOMPOSITION]\n");
181 pil_equations += (i == 0) ? "addressing_mode = " : format(OPERAND_PREFIX, static_cast<uint32_t>(i), " = ");
182
183 pil_equations += "(1 - PARSING_ERROR_EXCEPT_TAG_ERROR) * ("; // Error gating multiplicative term
184
185 std::vector<std::string> additive_terms;
186 for (const auto& sel_layout : sel_layout_breakdowns[i]) {
187 additive_terms.push_back(
188 format(SELECTOR_PREFIX, sel_layout.first, " * (", render_operand_layout_pil(sel_layout.second), ")"));
189 }
190 pil_equations +=
191 std::accumulate(std::next(additive_terms.begin()), additive_terms.end(), additive_terms[0], add_fold);
192 pil_equations += ");\n";
193 }
194 return pil_equations;
195}
196
198{
200
201 const auto& operand_type_sizes = simulation::testonly::get_operand_type_sizes();
202 const auto& wire_formats = simulation::testonly::get_instruction_wire_formats();
203
204 for (const auto& [opcode, format] : wire_formats) {
205 std::array<OperandLayout, NUM_OF_OPERANDS> operands_layout_array;
206 uint8_t op_idx = 1; // We start at index 1 because the index zero is reserved for addressing mode.
207 uint8_t byte_offset = 0;
208
209 for (const auto& operand : format) {
210 const auto operand_len = static_cast<uint8_t>(operand_type_sizes.at(operand));
211 const auto op_layout = OperandLayout{ .offset = byte_offset, .len = operand_len };
212
213 if (operand == OperandType::INDIRECT8 || operand == OperandType::INDIRECT16) {
214 operands_layout_array[0] = op_layout;
215 } else {
216 operands_layout_array[op_idx++] = op_layout;
217 }
218 byte_offset += operand_len;
219 }
220 mapping.insert(std::make_pair(opcode, operands_layout_array));
221 }
222 return mapping;
223}
224
225std::unordered_map<uint32_t, std::unordered_set<WireOpCode>> gen_op_idx_with_layout_to_opcode_subset(
226 const std::unordered_map<WireOpCode, std::array<OperandLayout, NUM_OF_OPERANDS>>& opcode_to_layouts)
227{
229
230 for (const auto& [wire_opcode, operand_layouts] : opcode_to_layouts) {
231 for (uint8_t i = 0; i < NUM_OF_OPERANDS; i++) {
232 const auto& layout = operand_layouts[i];
233 if (layout.len != 0) {
234 const auto key = encode_operand_idx_with_layout(i, layout.offset, layout.len);
235 if (op_idx_with_layout_to_subset.contains(key)) {
236 op_idx_with_layout_to_subset[key].insert(wire_opcode);
237 } else {
238 op_idx_with_layout_to_subset[key] = { wire_opcode };
239 }
240 }
241 }
242 }
243 return op_idx_with_layout_to_subset;
244}
245
246TEST(DecompositionSelectors, CodeGen)
247{
248 GTEST_SKIP(); // Comment out in order to code-generate.
250 gen_opcode_to_operands_layout();
251
252 // Map any given operand idx with layout to a subset of wire opcodes where
253 // we encode an operand idx with OperandLayout into a uint32_t as per function encode_operand_idx_with_layout().
255 gen_op_idx_with_layout_to_opcode_subset(opcode_to_layouts);
256
257 // A bit mask encodes a subset. Whenever an opcode is present in the subset, we toggle the bit
258 // at position represented by the opcode. Number of opcodes is smaller than 128 and therefore the bitmask
259 // fits in a uint128_t.
260 static_assert(static_cast<uint32_t>(WireOpCode::LAST_OPCODE_SENTINEL) < 128);
261
262 std::unordered_set<uint128_t> set_of_bitmasks;
263 std::unordered_map<uint32_t, uint128_t> op_idx_with_layout_to_bitmask;
264
265 for (const auto& [op_layout, subset] : op_idx_with_layout_to_subset) {
266 const auto encoded = encode_subset_wire_opcodes(subset);
267 set_of_bitmasks.insert(encoded);
268 op_idx_with_layout_to_bitmask.insert(std::make_pair(op_layout, encoded));
269 }
270
271 info("NUMBER OF SUBSETS: ", set_of_bitmasks.size());
272
273 // Is there any union of two disjoint subsets equal to another one?
274 bool partition_found = true;
275 std::vector<Partition> partitions;
276
277 // We try to remove partitions in a naive way, basically we remove the first encountered
278 // partition one after the other. This is by no way an exhaustive algorithm to find a hierarchy
279 // of partitions. This does the job as we have only one partition with the current AVM design.
280 while (partition_found) {
281 for (auto it1 = set_of_bitmasks.begin(); it1 != set_of_bitmasks.end(); it1++) {
282 auto it2 = it1;
283 for (it2++; it2 != set_of_bitmasks.end(); it2++) {
284 uint128_t sub_union = *it1 | *it2;
285 if ((*it1 & *it2) == 0 && set_of_bitmasks.contains(sub_union)) {
286 info("PARTITION FOUND! ", *it1, " ", *it2, " ", sub_union);
287 partitions.push_back(Partition{ .subset_1 = *it1, .subset_2 = *it2, .union_subset = sub_union });
288 set_of_bitmasks.erase(sub_union);
289 partition_found = true;
290 break;
291 }
292 partition_found = false;
293 }
294 if (partition_found) { // Mechanism to exit the outer loop
295 break;
296 }
297 }
298 }
299
300 info("NUMBER OF SUBSETS AFTER PARTITION REMOVAL: ", set_of_bitmasks.size());
301
302 std::vector<uint128_t> bitmasks_vector;
303 std::copy(set_of_bitmasks.begin(), set_of_bitmasks.end(), std::back_inserter(bitmasks_vector));
304
305 // Ensure a deterministic order
306 std::sort(bitmasks_vector.begin(), bitmasks_vector.end(), std::greater<>());
307
308 info("\n#################################");
309 info(" Precomputed Selectors Table:");
310 info("#################################\n");
311
312 info("constexpr size_t NUM_OP_DC_SELECTORS = ", set_of_bitmasks.size(), ";\n\n");
313
314 info("const std::unordered_map<WireOpCode, std::array<uint8_t, NUM_OP_DC_SELECTORS>> WireOpCode_DC_SELECTORS = "
315 "{");
316
317 const auto& wire_formats = simulation::testonly::get_instruction_wire_formats();
318 for (int i = 0; i < static_cast<int>(WireOpCode::LAST_OPCODE_SENTINEL); i++) {
319 const auto wire_opcode = static_cast<WireOpCode>(i);
320 if (wire_formats.contains(wire_opcode)) {
321 info("{WireOpCode::", wire_opcode, ", ", render_selector_array(wire_opcode, bitmasks_vector), "},");
322 }
323 }
324 info("};");
325
326 // Add subsets/bitmasks which were removed as unions at the end.
327 for (const auto& partition : partitions) {
328 bitmasks_vector.push_back(partition.union_subset);
329 }
330
331 std::unordered_map<uint128_t, size_t> bitmask_to_sel_idx;
332
333 for (size_t i = 0; i < bitmasks_vector.size(); i++) {
334 bitmask_to_sel_idx.insert(std::make_pair(bitmasks_vector[i], i));
335 }
336
337 // For each operand (index of the array), we store a vector of (selector, layout) corresponding to
338 // the decomposition of the operand with respect to bytes (bd1, bd2, ...)
339 // op_1 = sel_1 * (bd1 + bd2 * 2^8) + sel_4 * (bd5 + bd6 * 2^8)
340 std::array<std::vector<std::pair<size_t, OperandLayout>>, NUM_OF_OPERANDS> sel_layout_breakdowns;
341 for (const auto& [op_idx_with_layout, bitmask] : op_idx_with_layout_to_bitmask) {
342 uint8_t op_idx = get_op_idx(op_idx_with_layout);
343 OperandLayout layout = get_op_layout(op_idx_with_layout);
344 size_t sel_idx = bitmask_to_sel_idx.at(bitmask);
345 sel_layout_breakdowns[op_idx].emplace_back(std::make_pair(sel_idx, layout));
346 }
347
348 // For each sel-layout breakdown vector, we sort by increasing selector indices.
349 for (uint8_t i = 0; i < NUM_OF_OPERANDS; i++) {
350 std::sort(
351 sel_layout_breakdowns[i].begin(),
352 sel_layout_breakdowns[i].end(),
354 }
355
356 info("\n##################");
357 info("PIL Relations:");
358 info("##################\n");
359
360 info(render_partitions_pil(partitions, bitmask_to_sel_idx));
361 info(render_pil(sel_layout_breakdowns));
362}
363
364} // namespace
365} // namespace bb::avm2::constraining
std::string format(Args... args)
Definition log.hpp:23
#define info(...)
Definition log.hpp:93
FF a
FF b
ssize_t offset
Definition engine.cpp:62
TEST(AvmFixedVKTests, FixedVKCommitments)
Test that the fixed VK commitments agree with the ones computed from precomputed columns.
const std::unordered_map< OperandType, uint32_t > & get_operand_type_sizes()
const std::unordered_map< WireOpCode, std::vector< OperandType > > & get_instruction_wire_formats()
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
uint128_t subset_2
uint128_t subset_1
uint128_t union_subset
uint8_t len
unsigned __int128 uint128_t
Definition serialize.hpp:45