diff --git a/crypto3/libs/blueprint/include/nil/blueprint/bbf/circuit_builder.hpp b/crypto3/libs/blueprint/include/nil/blueprint/bbf/circuit_builder.hpp index 5032d2b153..b0db17642e 100644 --- a/crypto3/libs/blueprint/include/nil/blueprint/bbf/circuit_builder.hpp +++ b/crypto3/libs/blueprint/include/nil/blueprint/bbf/circuit_builder.hpp @@ -29,6 +29,7 @@ #define CRYPTO3_BLUEPRINT_PLONK_BBF_CIRCUIT_BUILDER_HPP #include +#include #include @@ -40,6 +41,7 @@ #include #include +#include #include #include @@ -113,18 +115,35 @@ namespace nil { // initialize params according to arguments witnesses_amount = witnesses; public_inputs_amount = public_inputs; - constants_amount = user_constants; + constants_amount = user_constants; rows_amount = rows; presets = crypto3::zk::snark::plonk_assignment_table(0,0,constants_amount,0); // intended extensible generate_constraints(); } + size_t create_table(const std::string& table_name, std::set& lookup_table_names, + const optimized_gates& gates) { + if (lookup_table_names.find(table_name) == lookup_table_names.end()) { + if (gates.dynamic_lookup_tables.find(table_name) != gates.dynamic_lookup_tables.end()) { + bp.reserve_dynamic_table(table_name); + } else { + bp.reserve_table(table_name); + } + lookup_table_names.insert(table_name); + } + size_t table_index = bp.get_reserved_indices().at(table_name); + return table_index; + } + void generate_constraints() { using generator = Component; using context_type = typename nil::blueprint::bbf::context; using constraint_type = crypto3::zk::snark::plonk_constraint; + using expression_type = typename constraint_type::base_type; + using copy_constraint_type = crypto3::zk::snark::plonk_copy_constraint; using lookup_constraint_type = crypto3::zk::snark::plonk_lookup_constraint; + using lookup_input_constraints_type = crypto3::zk::snark::lookup_input_constraints; using value_type = typename FieldType::value_type; using var = crypto3::zk::snark::plonk_variable; using TYPE = typename generator::TYPE; @@ -140,7 +159,8 @@ namespace nil { ); raw_input_type raw_input = {}; - auto v = std::tuple_cat(std::make_tuple(ct), generator::form_input(ct,raw_input), static_info_args_storage); + auto v = std::tuple_cat(std::make_tuple(ct), generator::form_input(ct,raw_input), + static_info_args_storage); std::make_from_tuple(v); // constants @@ -153,10 +173,19 @@ namespace nil { } } + // assure minimum inflation after padding + size_t usable_rows = std::pow(2, std::ceil(std::log2(rows_amount))) - 1; + ////////////////////////// Don't use 'ct' below this line, we just moved it!!! ///////////////////////////// gates_optimizer optimizer(std::move(ct)); optimized_gates gates = optimizer.optimize_gates(); + // TODO: replace with PLONK_SPECIAL_SELECTOR_ALL_USABLE_ROWS_SELECTED. + row_selector selector_column(usable_rows); + for (std::size_t i = 1; i < usable_rows; i++) + selector_column.set_row(i); + size_t full_selector_id = gates.add_selector(selector_column); + for(const auto& [selector_id, constraints] : gates.constraint_list) { /* std::cout << "GATE:\n"; @@ -176,25 +205,28 @@ namespace nil { } std::set lookup_table_names; - for(const auto& [selector_id, lookup_list] : gates.lookup_constraints) { + for (const auto& [selector_id, lookup_list] : gates.lookup_constraints) { std::vector lookup_gate; - for(const auto& single_lookup_constraint : lookup_list) { + for (const auto& single_lookup_constraint : lookup_list) { std::string table_name = single_lookup_constraint.first; - if (lookup_table_names.find(table_name) == lookup_table_names.end()) { - if (gates.dynamic_lookup_tables.find(table_name) != gates.dynamic_lookup_tables.end()) { - bp.reserve_dynamic_table(table_name); - } else { - bp.reserve_table(table_name); - } - lookup_table_names.insert(table_name); - } - std::size_t table_index = bp.get_reserved_indices().at(table_name); + size_t table_index = create_table(table_name, lookup_table_names, gates); lookup_gate.push_back({table_index, single_lookup_constraint.second}); } bp.add_lookup_gate(selector_id, lookup_gate); } + for (const auto& [table_name, grouped_lookups] : gates.grouped_lookups) { + size_t table_index = create_table(table_name, lookup_table_names, gates); + for (const auto& [ group_id, lookups] : grouped_lookups) { + lookup_input_constraints_type lookup_gate; + for (const auto& [selector_id, lookup_inputs] : lookups) { + lookup_gate += lookup_inputs * expression_type(var(selector_id, 0, false, var::column_type::selector)); + } + bp.add_lookup_gate(full_selector_id, (const std::vector&)lookup_gate); + } + } + // compatibility layer: dynamic lookup tables - continued for(const auto& [name, area] : gates.dynamic_lookup_tables) { bp.register_dynamic_table(name); @@ -215,7 +247,7 @@ namespace nil { } // this is where we pack lookup tables into constant columns and assign them selectors - std::size_t usable_rows = std::pow(2, std::ceil(std::log2(rows_amount))) - 1; // assure minimum inflation after padding + const auto &lookup_table_ids = bp.get_reserved_indices(); const auto &lookup_tables = bp.get_reserved_tables(); const auto &dynamic_tables = bp.get_reserved_dynamic_tables(); @@ -227,7 +259,6 @@ namespace nil { std::vector bp_lookup_tables(lookup_table_ids.size()); std::size_t start_constant_column = presets.constants_amount(); std::size_t prev_columns_number = 0; - int full_selector_id = -1; for(const auto &table_name : ordered_table_names) { const auto &table = lookup_tables.at(table_name); @@ -257,14 +288,9 @@ namespace nil { } // assure all added columns have same amount of usable_rows and need no resizement later for(std::size_t i = start_constant_column; i < start_constant_column + prev_columns_number; i++) - presets.constant(i,usable_rows-1) = 0; + presets.constant(i, usable_rows - 1) = 0; - if (full_selector_id == -1) { - row_selector selector_column(usable_rows); - for(std::size_t i = 1; i < usable_rows; i++) - selector_column.set_row(i); - full_selector_id = gates.add_selector(selector_column); - } + if (table_rows_number % usable_rows == 0) options_number--; std::size_t cur = 0; @@ -305,7 +331,7 @@ namespace nil { start_constant_column = cur_constant_column; continue; } else if (start_row + table_rows_number < usable_rows) { - if (prev_columns_number < table_columns_number) prev_columns_number = table_columns_number; + prev_columns_number = std::max(prev_columns_number, table_columns_number); } else if (table_rows_number < usable_rows) { start_row = 1; start_constant_column += prev_columns_number; diff --git a/crypto3/libs/blueprint/include/nil/blueprint/bbf/gate_optimizer.hpp b/crypto3/libs/blueprint/include/nil/blueprint/bbf/gate_optimizer.hpp index 68d440da47..c9d4c9f82c 100644 --- a/crypto3/libs/blueprint/include/nil/blueprint/bbf/gate_optimizer.hpp +++ b/crypto3/libs/blueprint/include/nil/blueprint/bbf/gate_optimizer.hpp @@ -57,7 +57,13 @@ namespace nil { std::unordered_map> constraint_list; std::vector copy_constraints; std::map, size_t>> dynamic_lookup_tables; - std::unordered_map> lookup_constraints; + + // Lookup constraints with single selector are stored here. + std::unordered_map> lookup_constraints; + + // The following lookup constraints are grouped with non-intersecting selectors. + // grouped_lookups[table_name][group_id][selector_id] maps to the lookup inputs for the given selector. + std::unordered_map>> grouped_lookups; // We will map each selector to the corresponding id. std::unordered_map, size_t> selectors_; @@ -67,6 +73,7 @@ namespace nil { size_t next_selector_id = selectors_.size(); if (iter == selectors_.end()) { selectors_.insert({selector, next_selector_id}); + // std::cout << "Added a selector " << selector << " which now has id " << next_selector_id << std::endl; return next_selector_id; } return iter->second; @@ -104,13 +111,14 @@ namespace nil { std::ostream& operator<<(std::ostream& os, const optimized_gates& gates) { for (const auto& [selector, id]: gates.selectors_) { auto iter = gates.constraint_list.find(id); - if (iter != gates.constraint_list.end()) { - os << "Selector #" << id << " " << selector << std::endl; - for (const auto &constraint : iter->second) { - os << constraint << std::endl; - } - os << "--------------------------------------------------------------" << std::endl; - } + os << "Selector #" << id << " " << selector << std::endl; + os << "Constraints: " << std::endl; + if (iter != gates.constraint_list.end()) { + for (const auto &constraint : iter->second) { + os << constraint << std::endl; + } + os << "--------------------------------------------------------------" << std::endl; + } } return os; } @@ -182,7 +190,6 @@ namespace nil { } for (const auto& [row_list, lookup_list] : lookup_constraints) { size_t id = result.add_selector(row_list); -//std::cout << "Processing lookup constraint with row list " << row_list << ", got id = " << id << std::endl; result.lookup_constraints[id] = std::move(lookup_list); } return result; @@ -192,8 +199,8 @@ namespace nil { optimized_gates result = context_to_gates(); // optimized_gates result = gates_storage_; // std::cout << "Before: \n\n" << result << std::endl; - // optimize_selectors_by_shifting(result); optimize_selectors_by_shifting(result); + optimize_lookups_by_grouping(result); // std::cout << "After: \n\n" << result << std::endl; return result; } @@ -201,6 +208,231 @@ namespace nil { private: + /** RLF (Recursive Largest First) algorithm for graph coloring. + * \param[in] adj - Adjacency list of the graph. + * \returns A vector that contains color of each vertex. + */ + std::vector colorGraph(const std::vector>& adj) { + size_t n = adj.size(); + if (n == 0) + return {}; + + // All vertices initially uncolored + std::vector color(n, std::numeric_limits::max()); + std::unordered_set uncolored; + for (size_t i = 0; i < n; i++) + uncolored.insert(i); + + size_t currentColor = 0; + + while (!uncolored.empty()) { + // Compute vertex degrees for uncolored vertices only. + std::vector degrees(n, 0); + for (size_t i: uncolored) { + for (size_t j : adj[i]) { + if (uncolored.find(j) != uncolored.end()) + degrees[i]++; + } + } + + // Step 1: Pick the vertex with the largest degree from the uncolored set + size_t startVertex = *std::max_element(uncolored.begin(), uncolored.end(), + [&](size_t a, size_t b) { + return degrees[a] < degrees[b]; + }); + + // Start a new color class + color[startVertex] = currentColor; + uncolored.erase(startVertex); + + // H is the set of vertices to consider adding + // Initially, it's all remaining uncolored vertices + std::unordered_set H = uncolored; + + // Throw out the neighbors of 'startVertex', those can't be included in the color class. + for (auto w : adj[startVertex]) { + H.erase(w); + } + + // Iteratively add vertices to the color class + while (!H.empty()) { + // Pick the vertex in H with the largest # of neighbours that are adjacent to + // some vertex in the color set S. + // Tie-break by highest degree + size_t candidate = 0; + bool found = false; + // best number of neighbors that are adjacent to vertices in class, I.E. are uncolored, + // but not in H. + size_t bestValue = 0; + // tie-break by number of neighbors not in the color class, + // I.E. the degree of the vertex in the uncolored graph. + size_t bestDegree = 0; + + for (auto v : uncolored) { + size_t val = 0; + for (size_t v2: adj[v]) { + // If v2 is an uncolored vertex, but it's not in H, that's because + // it is adjacent to a vertex in the color set. + if (uncolored.find(v2) != uncolored.end() && H.find(v2) == H.end()) + val++; + } + if (!found || val > bestValue || (val == bestValue && degrees[v] < bestDegree)) { + bestValue = val; + bestDegree = degrees[v]; + candidate = v; + found = true; + } + } + + // Add candidate to the color class + uncolored.erase(candidate); + color[candidate] = currentColor; + H.erase(candidate); + + for (auto w : adj[candidate]) { + H.erase(w); + } + } + + currentColor++; + } + + return color; + } + + /** Creates and returns a graph in the form of an adjucency list. + */ + std::vector> create_selector_intersection_graph( + const optimized_gates& gates, + const std::vector& used_selectors, + const std::unordered_map& selector_id_to_index) { + std::vector> adj; + // Create the graph. + adj.resize(used_selectors.size()); + for (const auto& [row_list1, selector_id1]: gates.selectors_) { + if (selector_id_to_index.find(selector_id1) == selector_id_to_index.end()) + continue; + for (const auto& [row_list2, selector_id2]: gates.selectors_) { + if (selector_id2 >= selector_id1 || selector_id_to_index.find(selector_id2) == selector_id_to_index.end()) + continue; + if (row_list1.intersects(row_list2)) { + // Add an edge. + adj[selector_id_to_index.at(selector_id1)].push_back(selector_id_to_index.at(selector_id2)); + adj[selector_id_to_index.at(selector_id2)].push_back(selector_id_to_index.at(selector_id1)); + } + } + } + return adj; + } + + std::vector> get_subgraph( + const std::vector>& graph, + const std::vector& all_lookup_selectors, + const std::unordered_map& selector_id_to_index, + const std::vector& subset_selectors) { + std::unordered_map selector_id_to_subset_index; + for (size_t i = 0; i < subset_selectors.size(); ++i) { + selector_id_to_subset_index[subset_selectors[i]] = i; + } + + std::vector> result(subset_selectors.size()); + for (size_t i = 0; i < subset_selectors.size(); ++i) { + // 'id' is actually an index of selector in the 'all_lookup_selectors'. + for (size_t id: graph[selector_id_to_index.at(subset_selectors[i])]) { + result[i].push_back(selector_id_to_subset_index[all_lookup_selectors[id]]); + } + } + return result; + } + + std::unordered_map group_selectors( + const std::vector>& graph, + const std::vector& all_lookup_selectors, + const std::unordered_map& selector_id_to_index, + const std::vector& used_selectors) { + std::vector> graph_subset = get_subgraph( + graph, all_lookup_selectors, selector_id_to_index, used_selectors); + + // coloring[i] is the group_id of used_selectors[i]. + std::vector coloring = colorGraph(graph_subset); + + // Now run over the returned coloring and map it back. + std::unordered_map result; + for (size_t i = 0; i < coloring.size(); ++i) { + result[used_selectors[i]] = coloring[i]; + } + return result; + } + + /** This function tries to reduce the number of lookups by grouping them. If 2 lookups use non-intersecting + * selectors, they can be merged into 1 like. + * Imagine lookup inputs {L0 ... Lm} with selector s1, and {l0 ... lm} with selector s2, then we can merge them into + * lookup inputs { s1 * L0 + s2 * l0, ... , s1 * Lm + s2 * lm } with selector that selects all the rows. + * We cannot optimally group the selectors into the minimal number of groups, that's an NP-complete problem + * called graph coloring problem. We will use Brooks' algorithm, it's some simple heuristic thing. + */ + void optimize_lookups_by_grouping(optimized_gates& gates) { + std::vector all_lookup_selectors; + std::unordered_map selector_id_to_index; + + for (const auto& [row_list, selector_id]: gates.selectors_) { + if (gates.lookup_constraints.find(selector_id) != gates.lookup_constraints.end()) { + all_lookup_selectors.push_back(selector_id); + selector_id_to_index[selector_id] = all_lookup_selectors.size() - 1; + } + } + + // Create an adjacency list of the whole large graph, since taking intersections of selectors is not super fast. + std::vector> adj = create_selector_intersection_graph( + gates, all_lookup_selectors, selector_id_to_index); + + // For each table, create the list of used selectors. + std::unordered_map> selectors_per_table; + for(const auto& [selector_id, lookup_list] : gates.lookup_constraints) { + for(const auto& single_lookup_constraint : lookup_list) { + const auto& table_name = single_lookup_constraint.first; + selectors_per_table[table_name].push_back(selector_id); + } + } + + // For each table, group the selectors. + + // Maps table name to a [map of selector id -> # of the group it belongs to]. + std::unordered_map> selector_groups; + std::unordered_map> group_sizes; + for (const auto& [table_name, selectors] : selectors_per_table) { + // Maps group_id -> # of selectors in it. + selector_groups[table_name] = group_selectors(adj, all_lookup_selectors, selector_id_to_index, selectors); + + // Count the size of each group, we need to not touch groups of size 1. + for (const auto& [selector_id, group_index]: selector_groups[table_name]) { + group_sizes[table_name][group_index]++; + } + } + + std::unordered_map> new_lookup_constraints; + + // Now merge all the lookups on selectors in the same group. + for (const auto& [selector_id, lookup_list] : gates.lookup_constraints) { + std::vector lookup_gate; + for (const auto& single_lookup_constraint : lookup_list) { + const std::string& table_name = single_lookup_constraint.first; + size_t group_id = selector_groups[table_name][selector_id]; + + // If the group size is 1, don't touch it. + if (group_sizes[table_name][group_id] == 1) { + new_lookup_constraints[selector_id].push_back( + {table_name, std::move(single_lookup_constraint.second)}); + } else { + gates.grouped_lookups[table_name][group_id][selector_id] = + std::move(single_lookup_constraint.second); + } + } + } + + gates.lookup_constraints = std::move(new_lookup_constraints); + } + /** This function tries to reduce the number of selectors required by rotating the constraints by +-1. */ void optimize_selectors_by_shifting(optimized_gates& gates) { @@ -212,11 +444,11 @@ namespace nil { std::vector> chosen_selectors = choose_selectors( left_shifts, right_shifts); - //std::cout << "The following selector shifts were selected: \n"; - //for (size_t i = 0; i < chosen_selectors.size(); ++i) { - // std::cout << "#" << i << " -> " << "#" << chosen_selectors[i].first << " shifted " << chosen_selectors[i].second << std::endl; - //} - //std::cout << std::endl; + //std::cout << "The following selector shifts were selected: \n"; + //for (size_t i = 0; i < chosen_selectors.size(); ++i) { + // std::cout << "#" << i << " -> " << "#" << chosen_selectors[i].first << " shifted " << chosen_selectors[i].second << std::endl; + //} + //std::cout << std::endl; // Maps the old selector ID to the new one, only for the selectors to be used. std::map new_selector_mapping; @@ -298,7 +530,7 @@ namespace nil { gates.constraint_list = std::move(result.constraint_list); gates.lookup_constraints = std::move(result.lookup_constraints); - gates.selectors_ = result.selectors_; + gates.selectors_ = result.selectors_; } /** @@ -329,7 +561,7 @@ namespace nil { } // For each node go to the left and right as far as possible. - // Then run over the chain of selectors and make the decisions. + // Then run over the chain of selectors and make the decisions. for (size_t i = 0; i < N; ++i) { // We may already have a decision for the current node. if (chosen_shifts[i].first != -1) @@ -377,7 +609,7 @@ namespace nil { chosen_shifts[chain[j - 1]] = {chain[j - 1], 0}; chosen_shifts[chain[j]] = {chain[j - 1], -1}; - // Check if chain[j - 2] can be skipped by rotating it to the right. + // Check if chain[j - 2] can be skipped by rotating it to the right. if (j >= 2 && right_shifts[chain[j - 2]] == chain[j - 1]) { chosen_shifts[chain[j - 2]] = {chain[j - 1], +1}; } diff --git a/crypto3/libs/blueprint/include/nil/blueprint/bbf/generic.hpp b/crypto3/libs/blueprint/include/nil/blueprint/bbf/generic.hpp index b3b9f57aa4..7112dd2003 100644 --- a/crypto3/libs/blueprint/include/nil/blueprint/bbf/generic.hpp +++ b/crypto3/libs/blueprint/include/nil/blueprint/bbf/generic.hpp @@ -468,8 +468,8 @@ namespace nil { } // accesible only at GenerationStage::CONSTRAINTS ! - void relative_lookup(std::vector &C, std::string table_name, std::size_t row) { - for(const TYPE c_part : C) { + void relative_lookup(const std::vector &C, std::string table_name, std::size_t row) { + for(const TYPE& c_part : C) { if (!c_part.is_relative()) { std::stringstream ss; ss << "Constraint " << c_part << " has absolute variables, cannot constrain."; @@ -479,8 +479,8 @@ namespace nil { add_lookup_constraint(table_name, C, row); } - void relative_lookup(std::vector &C, std::string table_name, std::size_t start_row, std::size_t end_row) { - for(const TYPE c_part : C) { + void relative_lookup(const std::vector &C, std::string table_name, std::size_t start_row, std::size_t end_row) { + for(const TYPE& c_part : C) { if (!c_part.is_relative()) { std::stringstream ss; ss << "Constraint " << c_part << " has absolute variables, cannot constrain."; @@ -629,7 +629,8 @@ namespace nil { lookup_constraints->at(key).second.set_row(stored_row); } - void add_lookup_constraint(std::string table_name, std::vector &C_rel, std::size_t start_row, std::size_t end_row) { + void add_lookup_constraint(const std::string& table_name, const std::vector &C_rel, + std::size_t start_row, std::size_t end_row) { std::size_t stored_start_row = start_row - (is_fresh ? row_shift : 0); std::size_t stored_end_row = end_row - (is_fresh ? row_shift : 0); constraint_id_type C_id = constraint_id_type(C_rel); diff --git a/crypto3/libs/blueprint/include/nil/blueprint/bbf/row_selector.hpp b/crypto3/libs/blueprint/include/nil/blueprint/bbf/row_selector.hpp index 0f301283a6..4ec8da4c5e 100644 --- a/crypto3/libs/blueprint/include/nil/blueprint/bbf/row_selector.hpp +++ b/crypto3/libs/blueprint/include/nil/blueprint/bbf/row_selector.hpp @@ -129,7 +129,7 @@ namespace nil { return used_rows_.count(); } - // This one is the maximal allowed index, I.E. the actual size. + // This one is the maximal allowed index, I.E. the actual size. std::size_t max_index() const { return used_rows_.size(); } @@ -138,6 +138,10 @@ namespace nil { return used_rows_.none(); } + bool intersects(const row_selector& other) const { + return used_rows_.intersects(other.used_rows_); + } + // TODO: delete this, if not used. /* row_selector& operator|=(const row_selector& other) { diff --git a/crypto3/libs/blueprint/test/bbf/gate_optimizer.cpp b/crypto3/libs/blueprint/test/bbf/gate_optimizer.cpp index 2a8c6d2eff..cf3ded7d50 100644 --- a/crypto3/libs/blueprint/test/bbf/gate_optimizer.cpp +++ b/crypto3/libs/blueprint/test/bbf/gate_optimizer.cpp @@ -42,53 +42,185 @@ using namespace nil::blueprint; BOOST_AUTO_TEST_SUITE(blueprint_bbf_gates_optimizer_test_suite) -BOOST_AUTO_TEST_CASE(blueprint_plonk_bbf_gates_optimizer_test) { +// This test checks how the gate optizimer reduces the number of selectors by shifing constraints by +-1 and re-using another +// selector. +BOOST_AUTO_TEST_CASE(blueprint_plonk_bbf_gates_optimizer_shifting_test) { using field_type = typename algebra::curves::pallas::base_field_type; using integral_type = typename field_type::integral_type; using value_type = typename field_type::value_type; using assignment_description_type = nil::crypto3::zk::snark::plonk_table_description; - using constraint_type = zk::snark::plonk_constraint; + using constraint_type = zk::snark::plonk_constraint; using context_type = bbf::context; - // Create just 1 of each column type, and 0 already used rows. - assignment_description_type desc(1, 1, 1, 1, 0, 0); + // Create just 1 of each column type, and 0 already used rows. + assignment_description_type desc(1, 1, 1, 1, 0, 0); - // Create a context that can use all 5 rows. - context_type c(desc, 6); - - constraint_type X1, X2, X3, X4, X5, X6; - c.allocate(X1, 0, 0, bbf::column_type::witness); - c.constrain(X1*(1-X1), "Left Constraint"); + // Create a context that can use all 6 rows. + context_type c(desc, 6); + + constraint_type X1, X2, X3, X4, X5, X6; + c.allocate(X1, 0, 0, bbf::column_type::witness); + c.constrain(X1*(1-X1), "Left Constraint"); - // Same thing on row 2. - c.allocate(X2, 0, 3, bbf::column_type::witness); - c.constrain(X2*(1-X2), "Left Constraint 2"); + // Same thing on row 2. + c.allocate(X2, 0, 3, bbf::column_type::witness); + c.constrain(X2*(1-X2), "Left Constraint 2"); - c.allocate(X3, 0, 1, bbf::column_type::witness); - c.constrain(X3*(2-X3), "Middle constraint 1"); + c.allocate(X3, 0, 1, bbf::column_type::witness); + c.constrain(X3*(2-X3), "Middle constraint 1"); - c.allocate(X4, 0, 4, bbf::column_type::witness); - c.constrain(X4*(2-X4), "Middle constraint 2"); + c.allocate(X4, 0, 4, bbf::column_type::witness); + c.constrain(X4*(2-X4), "Middle constraint 2"); - c.allocate(X5, 0, 2, bbf::column_type::witness); - c.constrain(X5*(3-X5), "Right constraint 1"); + c.allocate(X5, 0, 2, bbf::column_type::witness); + c.constrain(X5*(3-X5), "Right constraint 1"); - c.allocate(X6, 0, 5, bbf::column_type::witness); - c.constrain(X6*(3-X6), "Right constraint 2"); + c.allocate(X6, 0, 5, bbf::column_type::witness); + c.constrain(X6*(3-X6), "Right constraint 2"); - bbf::gates_optimizer optimizer(std::move(c)); - bbf::optimized_gates gates = optimizer.optimize_gates(); + bbf::gates_optimizer optimizer(std::move(c)); + bbf::optimized_gates gates = optimizer.optimize_gates(); - // We must have just 1 selector here, since these constraints can be shifted to match the same selector. - BOOST_CHECK_EQUAL(gates.selectors_.size(), 1); + // We must have just 1 selector here, since these constraints can be shifted to match the same selector. + BOOST_CHECK_EQUAL(gates.selectors_.size(), 1); - bbf::row_selector<> expected(8); - expected.set_row(1); - expected.set_row(4); + bbf::row_selector<> expected(8); + expected.set_row(1); + expected.set_row(4); - // This must be selector [1,4], since it's in the middle and the other 2 can be switched to that one. - BOOST_CHECK_EQUAL(gates.selectors_.begin()->first, expected); + // This must be selector [1,4], since it's in the middle and the other 2 can be switched to that one. + BOOST_CHECK_EQUAL(gates.selectors_.begin()->first, expected); +} + +// This test checks how the gate optizimer groups selectors used in lookups if they don't intersect. +BOOST_AUTO_TEST_CASE(blueprint_plonk_bbf_gates_optimizer_lookup_selector_grouping_test) { + using field_type = typename algebra::curves::pallas::base_field_type; + using integral_type = typename field_type::integral_type; + using value_type = typename field_type::value_type; + + using assignment_description_type = nil::crypto3::zk::snark::plonk_table_description; + using constraint_type = zk::snark::plonk_constraint; + using context_type = bbf::context; + using lookup_constraint_type = typename context_type::lookup_constraint_type; + using lookup_input_constraints_type = typename context_type::lookup_input_constraints_type; + + // Create 3 witness columns, and just 1 of each other column type, and 0 already used rows. + assignment_description_type desc(3, 1, 1, 1, 0, 0); + + // Create a context that can use all 6 rows. + context_type c(desc, 16); + constraint_type X, Y, Z; + c.allocate(X, 0, 0, bbf::column_type::witness); + c.allocate(Y, 1, 0, bbf::column_type::witness); + c.allocate(Z, 2, 0, bbf::column_type::witness); + + auto c1 = c.relativize(std::vector({X * X, Y * Y}), 0); + auto c2 = c.relativize(std::vector({Z * Z}), 0); + auto c3 = c.relativize(std::vector({(X - 1) * (X - 1), (Y - 1) * (Y - 1)}), 0); + + // We don't actually need to create the "Squares table" lookup table for this test, we can just create the + // lookups and optimize them. + c.relative_lookup(c1, "Squares Table", 0, 5); + c.relative_lookup(c2, "Squares Table", 2, 8); + c.relative_lookup(c3, "Squares Table", 6, 9); + + bbf::gates_optimizer optimizer(std::move(c)); + bbf::optimized_gates gates = optimizer.optimize_gates(); + + // Lookups 1 and 3 will be grouped, and the 2nd will stay as it is. + BOOST_CHECK_EQUAL(gates.lookup_constraints.size(), 1); + // We have a group lookup to a single table. + BOOST_CHECK_EQUAL(gates.grouped_lookups.size(), 1); + // And we have just 1 group inside. + BOOST_CHECK_EQUAL(gates.grouped_lookups["Squares Table"].size(), 1); + // The only group must have 2 selectors inside. + BOOST_CHECK_EQUAL(gates.grouped_lookups["Squares Table"].begin()->second.size(), 2); + + // Check that 2-nd constraint did not get grouped. + bbf::row_selector<> expected_selector_for_2nd(16); + expected_selector_for_2nd.set_interval(2, 8); + + size_t selector_id_for_2nd_constraint = gates.lookup_constraints.begin()->first; + BOOST_CHECK_EQUAL(gates.selectors_[expected_selector_for_2nd], selector_id_for_2nd_constraint); + BOOST_CHECK( + gates.lookup_constraints.begin()->second == + std::vector({ + std::make_pair("Squares Table", c2) + }) + ); + + // Check the grouped selectors. + bbf::row_selector<> expected_selector_for_1st(16); + expected_selector_for_1st.set_interval(0 ,5); + bbf::row_selector<> expected_selector_for_3rd(16); + expected_selector_for_3rd.set_interval(6, 9); + + for (auto [selector_id, li] : gates.grouped_lookups["Squares Table"].begin()->second) { + if (selector_id == gates.selectors_[expected_selector_for_1st]) { + BOOST_CHECK(li == c1); + } else if (selector_id == gates.selectors_[expected_selector_for_3rd]) { + BOOST_CHECK(li == c3); + } else { + BOOST_CHECK(false); + } + } +} + +// This test checks how the gate optizimer groups selectors used in lookups if they don't intersect. +BOOST_AUTO_TEST_CASE(blueprint_plonk_bbf_gates_optimizer_lookup_selector_larger_grouping_test) { + using field_type = typename algebra::curves::pallas::base_field_type; + using integral_type = typename field_type::integral_type; + using value_type = typename field_type::value_type; + + using assignment_description_type = nil::crypto3::zk::snark::plonk_table_description; + using constraint_type = zk::snark::plonk_constraint; + using context_type = bbf::context; + using lookup_constraint_type = typename context_type::lookup_constraint_type; + using lookup_input_constraints_type = typename context_type::lookup_input_constraints_type; + + // Create 3 witness columns, and just 1 of each other column type, and 0 already used rows. + assignment_description_type desc(3, 1, 1, 1, 0, 0); + + // Create a context that can use all 6 rows. + context_type c(desc, 16); + constraint_type X, Y, Z; + c.allocate(X, 0, 0, bbf::column_type::witness); + c.allocate(Y, 1, 0, bbf::column_type::witness); + c.allocate(Z, 2, 0, bbf::column_type::witness); + + auto c1 = c.relativize(std::vector({X * X, Y * Y}), 0); + auto c2 = c.relativize(std::vector({Z * Z}), 0); + auto c3 = c.relativize(std::vector({(X - 1) * (X - 1), (Y - 1) * (Y - 1)}), 0); + auto c4 = c.relativize(std::vector({(X - 2) * (X - 2), (Y - 2) * (Y - 2)}), 0); + auto c5 = c.relativize(std::vector({(X - 3) * (X - 2), (Y - 3) * (Y - 3)}), 0); + auto c6 = c.relativize(std::vector({(X - 4) * (X - 4), (Y - 4) * (Y - 4)}), 0); + auto c7 = c.relativize(std::vector({(X - 5) * (X - 5), (Y - 5) * (Y - 5)}), 0); + + // We want a wheel graph, a circle with a node in the middle. selector for 'c1' will intersect every other constraint, + // while each other one will only intersect with the one to the left and right of it. + // We don't actually need to create the "Squares table" lookup table for this test, we can just create the + // lookups and optimize them. + c.relative_lookup(c1, "Squares Table", 0, 14); + c.relative_lookup(c2, "Squares Table", 0, 2); + c.relative_lookup(c3, "Squares Table", 2, 4); + c.relative_lookup(c4, "Squares Table", 4, 6); + c.relative_lookup(c5, "Squares Table", 6, 8); + c.relative_lookup(c6, "Squares Table", 8, 10); + + c.relative_lookup(c7, "Squares Table", 10, 12); + c.relative_lookup(c7, "Squares Table", 0); + + bbf::gates_optimizer optimizer(std::move(c)); + bbf::optimized_gates gates = optimizer.optimize_gates(); + + // Lookups 'c1' will stay single. + BOOST_CHECK_EQUAL(gates.lookup_constraints.size(), 1); + // We have a group lookup to a single table. + BOOST_CHECK_EQUAL(gates.grouped_lookups.size(), 1); + + // And we have just 2 groups inside, [c2, c4, c6] and [c3, c5, c7]. + BOOST_CHECK_EQUAL(gates.grouped_lookups["Squares Table"].size(), 2); } BOOST_AUTO_TEST_SUITE_END() diff --git a/crypto3/libs/hash/test/CMakeLists.txt b/crypto3/libs/hash/test/CMakeLists.txt index 64e3c20bba..e67a041c07 100644 --- a/crypto3/libs/hash/test/CMakeLists.txt +++ b/crypto3/libs/hash/test/CMakeLists.txt @@ -58,6 +58,7 @@ set(TESTS_NAMES "static_digest" "poseidon" "hash_to_curve" + "type_traits" ) foreach(TEST_NAME ${TESTS_NAMES}) diff --git a/crypto3/libs/hash/test/type_traits.cpp b/crypto3/libs/hash/test/type_traits.cpp new file mode 100644 index 0000000000..dd64dc70a4 --- /dev/null +++ b/crypto3/libs/hash/test/type_traits.cpp @@ -0,0 +1,45 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2024 Martun Karapetyan +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#define BOOST_TEST_MODULE hash_type_traits_test + +#include +#include +#include +#include + +#include +#include + +using namespace nil::crypto3::algebra; + +BOOST_AUTO_TEST_SUITE(hash_type_traits_manual_tests) + +BOOST_AUTO_TEST_CASE(test_hash_traits) { + BOOST_ASSERT(nil::crypto3::detail::has_digest_type>::value); + BOOST_ASSERT(nil::crypto3::detail::has_digest_bits>::value); + BOOST_ASSERT(nil::crypto3::detail::is_hash>::value); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/crypto3/libs/math/include/nil/crypto3/math/kronecker_substitution.hpp b/crypto3/libs/math/include/nil/crypto3/math/kronecker_substitution.hpp old mode 100755 new mode 100644 diff --git a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial.hpp b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial.hpp index 6c4efd938a..28c1fa8d51 100644 --- a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial.hpp +++ b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial.hpp @@ -256,11 +256,11 @@ namespace nil { } void reserve(size_type _n) { - return val.reserve(_n); + val.reserve(_n); } void shrink_to_fit() BOOST_NOEXCEPT { - return val.shrink_to_fit(); + val.shrink_to_fit(); } reference operator[](size_type _n) BOOST_NOEXCEPT { @@ -359,11 +359,11 @@ namespace nil { } void resize(size_type _sz) { - return val.resize(_sz); + val.resize(_sz); } void resize(size_type _sz, const_reference _x) { - return val.resize(_sz, _x); + val.resize(_sz, _x); } void swap(polynomial& other) { diff --git a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs.hpp b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs.hpp index ed611bff3e..147ea6cbc4 100644 --- a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs.hpp +++ b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs.hpp @@ -238,10 +238,10 @@ namespace nil { return val.max_size(); } void reserve(size_type _n) { - return val.reserve(_n); + val.reserve(_n); } void shrink_to_fit() BOOST_NOEXCEPT { - return val.shrink_to_fit(); + val.shrink_to_fit(); } reference operator[](size_type _n) BOOST_NOEXCEPT { diff --git a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs_view.hpp b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs_view.hpp index d75db092e3..93106985d9 100644 --- a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs_view.hpp +++ b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_dfs_view.hpp @@ -189,10 +189,10 @@ namespace nil { return it.max_size(); } void reserve(size_type _n) { - return it.reserve(_n); + it.reserve(_n); } void shrink_to_fit() BOOST_NOEXCEPT { - return it.shrink_to_fit(); + it.shrink_to_fit(); } reference operator[](size_type _n) BOOST_NOEXCEPT { @@ -303,34 +303,11 @@ namespace nil { detail::basic_radix2_fft(it, omega_new); } - // void resize(size_type _sz, const_reference _x) { - // BOOST_ASSERT_MSG(_sz >= _d, "Can't restore polynomial in the future"); - // return val.resize(_sz, _x); - // } - void swap(polynomial_dfs_view& other) { it.swap(other.data()); std::swap(_d, other._d); } - // std::vector evaluate(const std::vector& value) const { - // typedef typename value_type::field_type FieldType; - // const std::size_t n = detail::power_of_two(this->_d); - // - // std::vector c(this->begin(), this->begin() + n); - // - // detail::basic_radix2_fft(c, (this->_omega).inversed()); - // - // std::vector result(value.size(), 0); - // auto end = c.end(); - // while (end != c.begin()) { - // for (size_t i = 0; i < value.size(); ++i) { - // result[i] = result[i] * value[i] + *--end; - // } - // } - // return result; - // } - FieldValueType evaluate(const FieldValueType& value) const { std::vector tmp = this->coefficients(); diff --git a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_view.hpp b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_view.hpp index 2c0d93ab4b..e448cd329d 100644 --- a/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_view.hpp +++ b/crypto3/libs/math/include/nil/crypto3/math/polynomial/polynomial_view.hpp @@ -182,10 +182,10 @@ namespace nil { return it.max_size(); } void reserve(size_type _n) { - return it.reserve(_n); + it.reserve(_n); } void shrink_to_fit() BOOST_NOEXCEPT { - return it.shrink_to_fit(); + it.shrink_to_fit(); } reference operator[](size_type _n) BOOST_NOEXCEPT { @@ -277,11 +277,11 @@ namespace nil { } void resize(size_type _sz) { - return it.resize(_sz); + it.resize(_sz); } void resize(size_type _sz, const_reference _x) { - return it.resize(_sz, _x); + it.resize(_sz, _x); } void swap(polynomial_view& other) { diff --git a/crypto3/libs/math/include/nil/crypto3/math/type_traits.hpp b/crypto3/libs/math/include/nil/crypto3/math/type_traits.hpp old mode 100755 new mode 100644 diff --git a/crypto3/libs/zk/include/nil/crypto3/zk/snark/arithmetization/plonk/lookup_constraint.hpp b/crypto3/libs/zk/include/nil/crypto3/zk/snark/arithmetization/plonk/lookup_constraint.hpp index 41f9b9943c..6ffaa2a31d 100644 --- a/crypto3/libs/zk/include/nil/crypto3/zk/snark/arithmetization/plonk/lookup_constraint.hpp +++ b/crypto3/libs/zk/include/nil/crypto3/zk/snark/arithmetization/plonk/lookup_constraint.hpp @@ -59,7 +59,7 @@ namespace nil { return *this; } - lookup_input_constraints operator*(const expression_type& other) { + lookup_input_constraints operator*(const expression_type& other) const { lookup_input_constraints result = *this; result *= other; return result; @@ -67,9 +67,11 @@ namespace nil { // Allow multiplication with any container of the same type. template - lookup_input_constraints& operator*=(const typename std::enable_if_t< - nil::crypto3::detail::is_range::value && std::is_same::value, - Container>& other) { + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints>& operator*=(const Container& other) { if (this->size() < other.size()) this->resize(other.size()); @@ -82,13 +84,76 @@ namespace nil { } template - lookup_input_constraints& operator*(const typename std::enable_if_t< - nil::crypto3::detail::is_range::value && std::is_same::value, - Container>& other) { + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints> + operator*(const Container& other) { lookup_input_constraints result = *this; result *= other; return result; } + + // Allow addition with any container of the same type. + template + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints>& operator+=(const Container& other) { + if (this->size() < other.size()) + this->resize(other.size()); + + auto it1 = this->begin(); + auto it2 = other.begin(); + for (; it2 != other.end(); ++it1, ++it2) { + *it1 += *it2; + } + return *this; + } + + template + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints> + operator+(const Container& other) { + lookup_input_constraints result = *this; + result += other; + return result; + } + + // Allow subtraction with any container of the same type. + template + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints>& operator-=(const Container& other) { + if (this->size() < other.size()) + this->resize(other.size()); + + auto it1 = this->begin(); + auto it2 = other.begin(); + for (; it2 != other.end(); ++it1, ++it2) { + *it1 -= *it2; + } + return *this; + } + + template + typename std::enable_if_t< + nil::crypto3::detail::is_range::value && ( + std::is_same::value || + std::is_same::value), + lookup_input_constraints> + operator-(const Container& other) { + lookup_input_constraints result = *this; + result -= other; + return result; + } }; diff --git a/crypto3/libs/zk/test/CMakeLists.txt b/crypto3/libs/zk/test/CMakeLists.txt index 4443ef17aa..8472be7fb0 100644 --- a/crypto3/libs/zk/test/CMakeLists.txt +++ b/crypto3/libs/zk/test/CMakeLists.txt @@ -54,6 +54,7 @@ set(TESTS_NAMES "commitment/kzg" "commitment/fold_polynomial" "commitment/proof_of_work" + "commitment/type_traits" "math/expression" @@ -65,6 +66,7 @@ set(TESTS_NAMES # "systems/plonk/pickles/to_field" # "systems/plonk/pickles/to_group" + "systems/plonk/type_traits" "systems/plonk/placeholder/placeholder_circuits" "systems/plonk/placeholder/placeholder_goldilocks" "systems/plonk/placeholder/placeholder_lookup_argument" diff --git a/crypto3/libs/zk/test/systems/plonk/type_traits.cpp b/crypto3/libs/zk/test/systems/plonk/type_traits.cpp new file mode 100644 index 0000000000..8c9bf5f5c3 --- /dev/null +++ b/crypto3/libs/zk/test/systems/plonk/type_traits.cpp @@ -0,0 +1,49 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2024 Martun Karapetyan +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#define BOOST_TEST_MODULE zk_type_traits_test + +#include + +#include +#include +#include + +#include + +#include + +using namespace nil::crypto3; + +BOOST_AUTO_TEST_SUITE(zk_type_traits_test_suite) + +BOOST_AUTO_TEST_CASE(zk_type_traits_basic_test) { + using curve_type = algebra::curves::bls12<381>; + using field_type = typename curve_type::scalar_field_type; + + static_assert( + nil::crypto3::detail::has_iterator>::value); +} + +BOOST_AUTO_TEST_SUITE_END()