-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathcollapse.nr
More file actions
237 lines (199 loc) · 9.31 KB
/
collapse.nr
File metadata and controls
237 lines (199 loc) · 9.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/// Collapses an array of `Option`s with sparse `Some` values into a `BoundedVec`, essentially unwrapping the `Option`s
/// and removing the `None` values.
///
/// For example, given:
/// `input: [some(3), none(), some(1)]`
/// this returns
/// `collapsed: [3, 1]`
pub fn collapse<T, let N: u32>(input: [Option<T>; N]) -> BoundedVec<T, N>
where
T: Eq,
{
// Computing the collapsed BoundedVec would result in a very large number of constraints, since we'd need to loop
// over the input array and conditionally write to a dynamic vec index, which is a very unfriendly pattern to the
// proving backend.
// Instead, we use an unconstrained function to produce the final collapsed array, along with some hints, and then
// verify that the input and collapsed arrays are equivalent.
let (collapsed, collapsed_to_input_index_mapping) = unsafe { get_collapse_hints(input) };
verify_collapse_hints(input, collapsed, collapsed_to_input_index_mapping);
collapsed
}
fn verify_collapse_hints<T, let N: u32>(
input: [Option<T>; N],
collapsed: BoundedVec<T, N>,
collapsed_to_input_index_mapping: BoundedVec<u32, N>,
)
where
T: Eq,
{
// collapsed should be a BoundedVec with all the non-none elements in input, in the same order. We need to lay down
// multiple constraints to guarantee this.
// First we check that the number of elements is correct
let mut count = 0;
for i in 0..N {
if input[i].is_some() {
count += 1;
}
}
assert_eq(count, collapsed.len(), "Wrong collapsed vec length");
// Then we check that all elements exist in the original array, and are in the same order. To do this we use the
// auxiliary collapsed_to_input_index_mapping array, which at index n contains the index in the input array that
// corresponds to the collapsed entry at index n.
// Example:
// - input: [some(3), none(), some(1)]
// - collapsed: [3, 1]
// - collapsed_to_input_index_mapping: [0, 2]
// These two arrays should therefore have the same length.
assert_eq(
collapsed.len(),
collapsed_to_input_index_mapping.len(),
"Collapse hint vec length mismatch",
);
// We now look at each collapsed entry and check that there is a valid equal entry in the input array.
let mut last_index = Option::none();
for i in 0..N {
if i < collapsed.len() {
let input_index = collapsed_to_input_index_mapping.get_unchecked(i);
assert(input_index < N, "Out of bounds index hint");
assert_eq(
collapsed.get_unchecked(i),
input[input_index].unwrap(),
"Wrong collapsed vec content",
);
// By requiring increasing input indices, we both guarantee that we're not looking at the same input
// element more than once, and that we're going over them in the original order.
if last_index.is_some() {
assert(input_index > last_index.unwrap_unchecked(), "Wrong collapsed vec order");
}
last_index = Option::some(input_index);
} else {
// BoundedVec assumes that the unused parts of the storage are zeroed out (e.g. in the Eq impl), so we make
// sure that this property holds.
assert_eq(
collapsed.get_unchecked(i),
std::mem::zeroed(),
"Dirty collapsed vec storage",
);
}
}
// We now know that:
// - all values in the collapsed array exist in the input array
// - the order of the collapsed values is the same as in the input array
// - no input value is present more than once in the collapsed array
// - the number of elements in the collapsed array is the same as in the input array.
// Therefore, the collapsed array is correct.
}
unconstrained fn get_collapse_hints<T, let N: u32>(
input: [Option<T>; N],
) -> (BoundedVec<T, N>, BoundedVec<u32, N>) {
let mut collapsed: BoundedVec<T, N> = BoundedVec::new();
let mut collapsed_to_input_index_mapping: BoundedVec<u32, N> = BoundedVec::new();
for i in 0..N {
if input[i].is_some() {
collapsed.push(input[i].unwrap_unchecked());
collapsed_to_input_index_mapping.push(i);
}
}
(collapsed, collapsed_to_input_index_mapping)
}
mod test {
use super::{collapse, verify_collapse_hints};
#[test]
unconstrained fn collapse_empty_array() {
let original: [Option<Field>; 2] = [Option::none(), Option::none()];
let collapsed = collapse(original);
assert_eq(collapsed.len(), 0);
}
#[test]
unconstrained fn collapse_non_sparse_array() {
let original = [Option::some(7), Option::some(3), Option::none()];
let collapsed = collapse(original);
assert_eq(collapsed.len(), 2);
assert_eq(collapsed.get(0), 7);
assert_eq(collapsed.get(1), 3);
}
#[test]
unconstrained fn collapse_sparse_array() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = collapse(original);
assert_eq(collapsed.len(), 2);
assert_eq(collapsed.get(0), 7);
assert_eq(collapsed.get(1), 3);
}
#[test]
unconstrained fn collapse_front_padding() {
let original =
[Option::none(), Option::none(), Option::some(7), Option::none(), Option::some(3)];
let collapsed = collapse(original);
assert_eq(collapsed.len(), 2);
assert_eq(collapsed.get(0), 7);
assert_eq(collapsed.get(1), 3);
}
#[test]
unconstrained fn collapse_back_padding() {
let original =
[Option::some(7), Option::none(), Option::some(3), Option::none(), Option::none()];
let collapsed = collapse(original);
assert_eq(collapsed.len(), 2);
assert_eq(collapsed.get(0), 7);
assert_eq(collapsed.get(1), 3);
}
#[test]
unconstrained fn verify_collapse_hints_good_hints() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7, 3]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0, 2]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Wrong collapsed vec length")]
unconstrained fn verify_collapse_hints_wrong_length() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Collapse hint vec length mismatch")]
unconstrained fn verify_collapse_hints_hint_length_mismatch() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7, 3]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Out of bounds index hint")]
unconstrained fn verify_collapse_hints_out_of_bounds_index_hint() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7, 3]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0, 5]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail)]
unconstrained fn verify_collapse_hints_hint_to_none() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7, 0]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0, 1]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Wrong collapsed vec content")]
unconstrained fn verify_collapse_hints_wrong_vec_content() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([7, 42]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0, 2]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Wrong collapsed vec order")]
unconstrained fn verify_collapse_hints_wrong_vec_order() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let collapsed = BoundedVec::from_array([3, 7]);
let collapsed_to_input_index_mapping = BoundedVec::from_array([2, 0]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
#[test(should_fail_with = "Dirty collapsed vec storage")]
unconstrained fn verify_collapse_hints_dirty_storage() {
let original = [Option::some(7), Option::none(), Option::some(3)];
let mut collapsed: BoundedVec<u32, 3> = BoundedVec::from_array([7, 3]);
// We have to use the unchecked setter as we're knowingly writing past the length, breaking its invariants.
collapsed.set_unchecked(2, 1);
let collapsed_to_input_index_mapping = BoundedVec::from_array([0, 2]);
verify_collapse_hints(original, collapsed, collapsed_to_input_index_mapping);
}
}