@@ -3,20 +3,43 @@ include "../precomputed.pil";
33
44/**
55 * A gadget that checks a merkle read or a write.
6- * One sibling node is processed per row, performing 1 hash if it's a merkle read
7- * or 2 hashes if it's a merkle write.
6+ * A merkle read consists in proving membership of a leaf (`read_leaf`) in a Merkle tree (against a given root).
7+ * For a merkle write, we additionally compute the new root of the tree with the new leaf (`updated_leaf`)
8+ * value. The sibling path is the same for both reads and writes and therefore the membership check and
9+ * the new root computation can be processed in "parallel".
10+ * One sibling node is processed per row, performing 1 hash (Poseidon2) if it is a merkle read
11+ * or 2 hashes if it is a merkle write.
812 *
913 * WARNING: This gadget will break if used with `tree_height >= 254`
1014 *
15+ * Precondition: tree_height/path_len < 254
16+ *
17+ * Errors: None
18+ *
19+ * Number of rows per computation: tree_height/path_len
20+ *
1121 * Read usage:
12- * caller_sel { read_leaf, leaf_index, tree_height, expected_root }
13- * in merkle_check.start { merkle_check.read_node, merkle_check.index, merkle_check.path_len, merkle_check.read_root }
22+ * caller_sel {
23+ * read_leaf, leaf_index, tree_height, expected_root
24+ * } in merkle_check.start {
25+ * merkle_check.read_node, merkle_check.index, merkle_check.path_len, merkle_check.read_root
26+ * };
1427 *
1528 * Write usage:
16- * caller_sel { one, prev_leaf_value, next_leaf_value,
17- * leaf_index, tree_height, prev_root, next_root }
18- * in merkle_check.start { merkle_check.write, merkle_check.read_node, merkle_check.write_node,
19- * merkle_check.index, merkle_check.path_len, merkle_check.read_root, merkle_check.write_root }
29+ * caller_sel {
30+ * one, current_leaf, updated_leaf,
31+ * leaf_index, tree_height, prev_root, next_root
32+ * } in merkle_check.start {
33+ * merkle_check.write, merkle_check.read_node, merkle_check.write_node,
34+ * merkle_check.index, merkle_check.path_len, merkle_check.read_root, merkle_check.write_root
35+ * };
36+ *
37+ * Read/Write flexible usage: One can call the write usage variant with a boolean in the first column
38+ * to toggle between read and write. However, the caller needs to ensure that trace generation will
39+ * set `updated_leaf` and `next_root` to 0 when `write` is 0.
40+ *
41+ * USAGE WARNING: Never invoke this gadget using `merkle_check.write` as destination selector. This would be
42+ * completely under-constrained as we do not enforce `sel == 1` when `write == 1`.
2043 *
2144 * Common inputs
2245 * @column read_node The value of the node being read. When `start == 1`, this is the leaf.
@@ -27,7 +50,10 @@ include "../precomputed.pil";
2750 * Write specific inputs
2851 * @column write Whether this is a write operation.
2952 * @column write_node The value of the node being written. When `start == 1`, this is the new leaf.
30- * @column write_root The merkle root that results of replacing the leaf with the new leaf at the index provided in the read_root. This will be propagated down to be matched internally on `end`.
53+ *
54+ * Write specific output
55+ * @column write_root The merkle root that results of replacing the leaf with the new leaf at the index provided
56+ * in the read_root. This will be propagated down to be matched internally on `end`.
3157 *
3258 * Internals/hints
3359 * @column sibling The value of the sibling node to be hashed with `read_node`.
@@ -49,110 +75,121 @@ include "../precomputed.pil";
4975 * +-----------+-------+----------+---------+---------------+----------------+-----------------+------------------+-----------+-------+------------+-----------------+------------------+-------------------+------------+-------+-----+-----+
5076 */
5177namespace merkle_check;
52- pol commit sel;
53- sel * (1 - sel) = 0;
54- // No relations will be checked if this identity is satisfied.
55- #[skippable_if]
78+ #[skippable_if]
5679 sel = 0;
5780
58- // If the current row is not active, then there are no more active rows after that.
59- // Note that sel cannot be activated in the first row as sel' is defined.
60- // As a consequence, if a row is activated (sel == 1) somewhere in this sub-trace, then
61- // the activated rows start from the second row and are contiguous.
62- #[TRACE_CONTINUITY]
63- (1 - precomputed.first_row) * (1 - sel) * sel' = 0;
81+ pol commit sel; // @boolean
82+ sel * (1 - sel) = 0;
6483
6584 // Common inputs - These must be looked up both on read and write
6685 pol commit read_node;
6786 pol commit index;
6887 pol commit path_len;
6988 pol commit read_root;
7089
71- // Write specific inputs - These are only looked up when we want to update the root
72- pol commit write;
73- write * (1 - write) = 0; // bool
90+ // Write specific inputs/outputs - These are only looked up when we want to update the root
91+ pol commit write; // This should be a boolean passed by the caller. For a read usage, it is under-constrained but read
92+ // relevant values are constrained. For write, the caller must pass 1.
7493 pol commit write_node;
7594 pol commit write_root;
7695
7796 // Hints
7897 pol commit sibling;
7998
8099 // Boundaries
81- pol commit start;
82- pol commit end;
83- pol NOT_END = sel * (1 - end);
84- start * (1 - start) = 0; // bool
85- end * (1 - end) = 0; // bool
86-
87- // only one of end or first_row can be 1
88- // end can't be 1 for first row
89- end * precomputed.first_row = 0;
90- // LATCH_CONDITION is true if either end is 1 or first_row is 1
91- pol LATCH_CONDITION = end + precomputed.first_row;
92-
93- #[START_AFTER_LATCH]
94- sel' * (start' - LATCH_CONDITION) = 0;
100+ pol commit start; // @boolean
101+ pol commit end; // @boolean
102+ start * (1 - start) = 0;
103+ end * (1 - end) = 0;
95104
96- // Selector must be 1 in end row
97- #[SELECTOR_ON_END]
98- end * (1 - sel) = 0;
105+ // End when remaining path reaches 1. In other words, (path_len == 1) <==> (end == 1)
106+ pol PATH_LEN_MIN_ONE = path_len - 1;
107+ pol commit path_len_min_one_inv;
108+ #[END_IFF_REM_PATH_EMPTY]
109+ sel * (PATH_LEN_MIN_ONE * (end * (1 - path_len_min_one_inv) + path_len_min_one_inv) - 1 + end) = 0;
110+
111+ // This prevents a computation to be stopped prematurely, i.e., truncating last rows before
112+ // we reach a row where end == 1. Note that #[END_IFF_REM_PATH_EMPTY] is conditioned on `sel == 1`
113+ // and a malicious prover could prematurely stop by setting `sel == 0` and `end == 0`.
114+ #[COMPUTATION_FINISH_AT_END]
115+ sel * (1 - sel') * (1 - end) = 0;
116+
117+ // This gadget is looked up based on `start` destination selector and therefore we
118+ // need to ensure that this computation is performed on the active (`sel == 1`) rows. Otherwise,
119+ // a malicious prover could circumvent most of the constraints. Note that #[START_AFTER_LATCH]
120+ // does not prevent this from happening.
121+ // In addition, we want `end == 1` to be on an active row as well.
122+ #[SELECTOR_ON_START_OR_END]
123+ (start + end) * (1 - sel) = 0;
124+
125+ // LATCH_CONDITION is true if either `end` is 1 or `first_row` is 1
126+ // LATCH_CONDITION is a boolean because `end` cannot be 1 on the first row (as `sel == 1` when `end == 1`
127+ // and `sel == 0` on first row)
128+ pol LATCH_CONDITION = end + precomputed.first_row;
99129
100130 #[PROPAGATE_READ_ROOT]
101- NOT_END * (read_root' - read_root) = 0;
131+ (1 - LATCH_CONDITION) * (read_root' - read_root) = 0;
102132 #[PROPAGATE_WRITE]
103- NOT_END * (write' - write) = 0;
133+ (1 - LATCH_CONDITION) * (write' - write) = 0;
104134 #[PROPAGATE_WRITE_ROOT]
105- NOT_END * (write_root' - write_root) = 0;
135+ (1 - LATCH_CONDITION) * (write_root' - write_root) = 0;
106136
107137 // If we are not done, the path_len decrements by 1
108138 #[PATH_LEN_DECREMENTS]
109- NOT_END * (path_len' - path_len + 1) = 0;
139+ sel * (1 - end) * (path_len' - path_len + 1) = 0;
110140
111- // End when remaining path reaches 1. In other words, (path_len == 1) <==> (end == 1)
112- pol REMAINING_PATH_LEN = path_len - 1;
113-
114- pol commit remaining_path_len_inv;
115- #[END_WHEN_PATH_EMPTY]
116- sel * (REMAINING_PATH_LEN * (end * (1 - remaining_path_len_inv) + remaining_path_len_inv) - 1 + end) = 0;
117-
118- // index_is_even is constrained to be correct by the NEXT_INDEX_IS_HALVED and FINAL_INDEX_IS_0_OR_1 constraints
119- pol commit index_is_even;
141+ // index_is_even is constrained to be correct by the NEXT_INDEX_IS_HALVED and FINAL_INDEX_IS_ODD_BOOLEAN constraints
142+ pol commit index_is_even; // @boolean
120143 index_is_even * (1 - index_is_even) = 0;
121144 pol INDEX_IS_ODD = (1 - index_is_even);
122145 // The index into the next layer is half the current index.
123146 // We don't need to worry about underflowing the field since (index - INDEX_IS_ODD)
124147 // will be even (over the integers) and as the field is not of characteristic 2, index' == index / 2 over the integers
125148 #[NEXT_INDEX_IS_HALVED]
126- NOT_END * (index' * 2 + INDEX_IS_ODD - index) = 0;
149+ sel * (1 - end) * (2 * index' + INDEX_IS_ODD - index) = 0;
127150
128- // Ensure that the final index is 0 or 1.
151+ // Ensure that the final index is 0 or 1 by setting it to INDEX_IS_ODD .
129152 // This ensures that the previous layer cannot overflow the field in the halving constraint
130- // when doing `index' * 2 `. This propagates backwards ensuring that no
153+ // when doing `2 * index' `. This propagates backwards ensuring that no
131154 // layer can overflow on the halving constraint's multiplication by 2 as long as
132155 // tree_height < 254.
133- #[FINAL_INDEX_IS_0_OR_1]
134- end * (index * (1 - index)) = 0;
135- // NOTE: index_is_even is essentially a vertical bit-decomposition of leaf_index.
156+ // In addition, this relation constrains the value of `index_is_even` for the last row. without
157+ // it `index_is_even` would be under-constrained and a malicious prover could swap the sibling
158+ // and the node at the top Merkle tree layer in some relations further below.
159+ #[FINAL_INDEX_EQUAL_TO_FIRST_BIT]
160+ end * (index - INDEX_IS_ODD) = 0;
161+
162+ // The bit decomposition of leaf_index corresponds to the INDEX_IS_ODD bits. Namely,
163+ // for an initial path_len == LEN, we have:
164+ // leaf_index = sum_{i=0}^{LEN-1} INDEX_IS_ODD[i] * 2^i
165+ // where INDEX_IS_ODD[i] denotes the value of INDEX_IS_ODD at row i (incrementing
166+ // top-down and starting from 0). Therefore, any leaf_index < 2^LEN will be decomposed
167+ // and constrained correctly.
136168
137169 // left_node and right_node are sent to poseidon2
138170 // The constraints below arrange node and sibling for read and write into proper left/right order
139171 pol commit read_left_node;
140172 pol commit read_right_node;
141173 pol commit write_left_node;
142174 pol commit write_right_node;
143- // This is accomplished by using index_is_even to toggle the (left_node - right_node) term.
144- // If index is even, left_node (to send to poseidon2) is node and right_node is sibling.
145- // And vice-versa.
146- #[ASSIGN_NODE_LEFT_OR_RIGHT_READ]
147- sel * (index_is_even * (read_left_node - read_right_node) + read_right_node - read_node) = 0;
148- #[ASSIGN_SIBLING_LEFT_OR_RIGHT_READ]
149- sel * (index_is_even * (read_right_node - read_left_node) + read_left_node - sibling) = 0;
150-
151- #[ASSIGN_NODE_LEFT_OR_RIGHT_WRITE]
152- write * (index_is_even * (write_left_node - write_right_node) + write_right_node - write_node) = 0;
153- #[ASSIGN_SIBLING_LEFT_OR_RIGHT_WRITE]
154- write * (index_is_even * (write_right_node - write_left_node) + write_left_node - sibling) = 0;
155- // NOTE: don't think these can be safely combined
175+
176+ // This is accomplished by using the value of `index_is_even`. If `index_is_even == 1`, it means that
177+ // the current node position at the current layer is even. As a layer index starts at 0, this means that
178+ // the current node is the left child of its parent and the sibling is the right child.
179+ // Conversly, if `index_is_even == 0`, the curren node is the right child of its parent and the sibling
180+ // is the left child.
181+ // For `index_is_even == 1`, we assign `read_left_node := read_node` and `read_right_node := sibling`.
182+ // For `index_is_even == 0`, we assign `read_left_node := sibling` and `read_right_node := read_node`.
183+ #[READ_LEFT_NODE]
184+ read_left_node = index_is_even * (read_node - sibling) + sibling;
185+ #[READ_RIGHT_NODE]
186+ read_right_node = index_is_even * (sibling - read_node) + read_node;
187+
188+ // Same as above for write (but conditioned on `write == 1`).
189+ #[WRITE_LEFT_NODE]
190+ write_left_node = write * (index_is_even * (write_node - sibling) + sibling);
191+ #[WRITE_RIGHT_NODE]
192+ write_right_node = write * (index_is_even * (sibling - write_node) + write_node);
156193
157194 // output_hash = hash(left_node, right_node)
158195 // if index_is_even: output_hash = hash(node, sibling)
@@ -163,18 +200,24 @@ namespace merkle_check;
163200
164201 // Lookup to the full poseidon2 gadget
165202 #[MERKLE_POSEIDON2_READ]
166- sel { read_left_node, read_right_node, /*input_2=*/ precomputed.zero, /*start=1=*/ sel, read_output_hash }
167- in poseidon2_hash.end { poseidon2_hash.input_0, poseidon2_hash.input_1, poseidon2_hash.input_2, poseidon2_hash.start, poseidon2_hash.output };
203+ sel {
204+ read_left_node, read_right_node, /*input_2=*/ precomputed.zero, /*start=1=*/ sel, read_output_hash
205+ } in poseidon2_hash.end {
206+ poseidon2_hash.input_0, poseidon2_hash.input_1, poseidon2_hash.input_2, poseidon2_hash.start, poseidon2_hash.output
207+ };
168208
169209 #[MERKLE_POSEIDON2_WRITE]
170- write { write_left_node, write_right_node, /*input_2=*/ precomputed.zero, /*start=1=*/ sel, write_output_hash }
171- in poseidon2_hash.end { poseidon2_hash.input_0, poseidon2_hash.input_1, poseidon2_hash.input_2, poseidon2_hash.start, poseidon2_hash.output };
210+ write {
211+ write_left_node, write_right_node, /*input_2=*/ precomputed.zero, /*start=1=*/ write, write_output_hash
212+ } in poseidon2_hash.end {
213+ poseidon2_hash.input_0, poseidon2_hash.input_1, poseidon2_hash.input_2, poseidon2_hash.start, poseidon2_hash.output
214+ };
172215
173216 // If we are not done, this row's output_hash is the next row's node
174217 #[OUTPUT_HASH_IS_NEXT_ROWS_READ_NODE]
175- NOT_END * (read_node' - read_output_hash) = 0;
218+ (1 - LATCH_CONDITION) * (read_node' - read_output_hash) = 0;
176219 #[OUTPUT_HASH_IS_NEXT_ROWS_WRITE_NODE]
177- NOT_END * (write_node' - write_output_hash) = 0;
220+ (1 - LATCH_CONDITION) * (write_node' - write_output_hash) = 0;
178221
179222 // If we are done, the output hash is the root
180223 #[READ_OUTPUT_HASH_IS_READ_ROOT]
0 commit comments