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