@@ -73,46 +73,171 @@ impl HonestDealerDlogLookupTable {
73
73
}
74
74
}
75
75
76
+ /*
77
+ * To minimize the memory stored in the BSGS table, instead of storing
78
+ * elements of Gt directly (576 bytes) we first hash them using
79
+ * SHA-224. This reduces the memory consumption by approximately a
80
+ * factor of 20, and is quite fast to compute especially as production
81
+ * node machines all have SHA-NI support.
82
+ *
83
+ * Rust's HashMap usually hashes using SipHash with a random seed.
84
+ * This prevents against denial of service attacks caused by malicious
85
+ * keys. Here we instead use a very simple hash which is safe because
86
+ * we know that all of our keys were generated via a cryptographic hash
87
+ * already. This also disables the randomization step. This is safe
88
+ * because we only ever hash the serialization of elements of Gt g*i,
89
+ * where i is a smallish integer that depends on various NIDKG
90
+ * parameters.
91
+ */
92
+
93
+ struct ShiftXorHasher {
94
+ state : u64 ,
95
+ }
96
+
97
+ impl std:: hash:: Hasher for ShiftXorHasher {
98
+ fn write ( & mut self , bytes : & [ u8 ] ) {
99
+ for & byte in bytes {
100
+ self . state = self . state . rotate_left ( 8 ) ^ u64:: from ( byte) ;
101
+ }
102
+ }
103
+
104
+ fn finish ( & self ) -> u64 {
105
+ self . state
106
+ }
107
+ }
108
+
109
+ struct BuildShiftXorHasher ;
110
+
111
+ impl std:: hash:: BuildHasher for BuildShiftXorHasher {
112
+ type Hasher = ShiftXorHasher ;
113
+ fn build_hasher ( & self ) -> ShiftXorHasher {
114
+ ShiftXorHasher { state : 0 }
115
+ }
116
+ }
117
+
118
+ struct BabyStepGiantStepTable {
119
+ // Table storing the baby steps
120
+ table : std:: collections:: HashMap < [ u8 ; Self :: GT_REPR_SIZE ] , usize , BuildShiftXorHasher > ,
121
+ }
122
+
123
+ /// The table for storing the baby steps of BSGS
124
+ ///
125
+ /// TODO(CRP-2308) use a better data structure than HashMap here.
126
+ impl BabyStepGiantStepTable {
127
+ const GT_REPR_SIZE : usize = 28 ;
128
+
129
+ fn hash_gt ( gt : & Gt ) -> [ u8 ; Self :: GT_REPR_SIZE ] {
130
+ ic_crypto_sha2:: Sha224 :: hash ( & gt. tag ( ) )
131
+ }
132
+
133
+ /// Return a table size appropriate for solving BSGS in [0,range) while
134
+ /// keeping within the given table size constraints.
135
+ ///
136
+ /// The default size is the square root of the range, as is usual for BSGS.
137
+ /// If the memory limit allows for it, instead a small multiple of the
138
+ /// sqrt is used. However we always use at least the square root of the range,
139
+ /// since decreasing below that increases the costs of the online step.
140
+ fn compute_table_size ( range : usize , max_mbytes : usize , max_table_mul : usize ) -> usize {
141
+ let sqrt = ( range as f64 ) . sqrt ( ) . ceil ( ) as usize ;
142
+
143
+ // Estimate of HashMap overhead from https://ntietz.com/blog/rust-hashmap-overhead/
144
+ let hash_table_overhead = 1.73_f64 ;
145
+ let hash_table_storage = Self :: GT_REPR_SIZE + 8 ;
146
+
147
+ let storage = ( hash_table_storage as f64 ) * hash_table_overhead * ( sqrt as f64 ) ;
148
+
149
+ let max_bytes = max_mbytes * 1024 * 1024 ;
150
+
151
+ for mult in ( 1 ..=max_table_mul) . rev ( ) {
152
+ let est_storage = ( ( mult as f64 ) * storage) as usize ;
153
+
154
+ if est_storage < max_bytes {
155
+ return mult * sqrt;
156
+ }
157
+ }
158
+
159
+ sqrt
160
+ }
161
+
162
+ /// Returns the table plus the giant step
163
+ fn new ( base : & Gt , table_size : usize ) -> ( Self , Gt ) {
164
+ let mut table =
165
+ std:: collections:: HashMap :: with_capacity_and_hasher ( table_size, BuildShiftXorHasher ) ;
166
+ let mut accum = Gt :: identity ( ) ;
167
+
168
+ for i in 0 ..table_size {
169
+ let hash = Self :: hash_gt ( & accum) ;
170
+ table. insert ( hash, i) ;
171
+ accum += base;
172
+ }
173
+
174
+ ( Self { table } , accum. neg ( ) )
175
+ }
176
+
177
+ /// Return the value if gt exists in this table
178
+ fn get ( & self , gt : & Gt ) -> Option < usize > {
179
+ self . table . get ( & Self :: hash_gt ( gt) ) . copied ( )
180
+ }
181
+ }
182
+
76
183
pub struct BabyStepGiantStep {
77
184
// Table storing the baby steps
78
- table : std :: collections :: HashMap < [ u8 ; Gt :: BYTES ] , isize > ,
185
+ table : BabyStepGiantStepTable ,
79
186
// Group element `G * -n`, where `G` is the base element used for the discrete log problem.
80
187
giant_step : Gt ,
81
188
// Group element used as an offset to scale down the discrete log in the `0..range`.
82
189
offset : Gt ,
83
190
// Size of the baby steps table
84
- n : isize ,
191
+ n : usize ,
192
+ // Number of giant steps to take
193
+ giant_steps : usize ,
85
194
// Integer representing the smallest discrete log in the search space.
86
195
lo : isize ,
87
- // Size of the search space.
88
- range : isize ,
89
196
}
90
197
91
198
impl BabyStepGiantStep {
92
199
/// Set up a table for Baby-Step Giant-step to solve the discrete logarithm
93
- /// problem in the range `lo..lo+range` with respect to base element `base``.
94
- pub fn new ( base : & Gt , lo : isize , range : isize ) -> Self {
95
- let n = ( range as f64 ) . sqrt ( ) . ceil ( ) as isize ;
96
-
97
- let mut table = std:: collections:: HashMap :: new ( ) ;
98
- let mut accum = Gt :: identity ( ) ;
99
-
100
- for i in 0 ..n {
101
- table. insert ( accum. tag ( ) , i) ;
102
- accum += base;
103
- }
200
+ /// problem in the range `[lo..lo+range)` with respect to base element `base`.
201
+ ///
202
+ /// To reduce the cost of the online search phase of the algorith, this
203
+ /// implementation supports using a larger table than the typical `sqrt(n)`.
204
+ /// The parameters `max_mbytes` and `max_table_mul` control how large a
205
+ /// table is created. We always create at least a `sqrt(n)` sized table, but
206
+ /// try to create instead `k*sqrt(n)` sized if the parameters allow.
207
+ ///
208
+ /// `max_table_mul` controls the maximum value of `k`. Setting
209
+ /// `max_table_mul` to zero is effectively ignored.
210
+ ///
211
+ /// `max_mbytes` sets a limit on the total memory consumed by the table.
212
+ /// This is not precise as, while we try to account for the overhead of the
213
+ /// data structure used, it is based only on some rough estimates.
214
+ pub fn new (
215
+ base : & Gt ,
216
+ lo : isize ,
217
+ range : usize ,
218
+ max_mbytes : usize ,
219
+ max_table_mul : usize ,
220
+ ) -> Self {
221
+ let table_size =
222
+ BabyStepGiantStepTable :: compute_table_size ( range, max_mbytes, max_table_mul) ;
223
+
224
+ let giant_steps = if range > 0 && table_size > 0 {
225
+ ( range + table_size - 1 ) / table_size
226
+ } else {
227
+ 0
228
+ } ;
104
229
105
- let giant_step = accum . neg ( ) ;
230
+ let ( table , giant_step) = BabyStepGiantStepTable :: new ( base , table_size ) ;
106
231
107
232
let offset = base * Scalar :: from_isize ( lo) . neg ( ) ;
108
233
109
234
Self {
110
235
table,
111
236
giant_step,
112
237
offset,
113
- n,
238
+ n : table_size,
239
+ giant_steps,
114
240
lo,
115
- range,
116
241
}
117
242
}
118
243
@@ -121,18 +246,12 @@ impl BabyStepGiantStep {
121
246
///
122
247
/// Returns `None` if the discrete logarithm is not in the searched range.
123
248
pub fn solve ( & self , tgt : & Gt ) -> Option < Scalar > {
124
- let baby_steps = if self . range > 0 && self . n >= 0 {
125
- self . range / self . n
126
- } else {
127
- 0
128
- } ;
129
-
130
249
let mut step = tgt + & self . offset ;
131
250
132
- for baby_step in 0 ..baby_steps {
133
- if let Some ( i) = self . table . get ( & step. tag ( ) ) {
134
- let x = self . lo + self . n * baby_step ;
135
- return Some ( Scalar :: from_isize ( x + i ) ) ;
251
+ for giant_step in 0 ..self . giant_steps {
252
+ if let Some ( i) = self . table . get ( & step) {
253
+ let x = self . lo + ( i + self . n * giant_step ) as isize ;
254
+ return Some ( Scalar :: from_isize ( x) ) ;
136
255
}
137
256
step += & self . giant_step ;
138
257
}
@@ -148,12 +267,28 @@ pub struct CheatingDealerDlogSolver {
148
267
}
149
268
150
269
impl CheatingDealerDlogSolver {
270
+ const MAX_TABLE_MBYTES : usize = 2 * 1024 ; // 2 GiB
271
+
272
+ // We limit the maximum table size when compiling without optimizations
273
+ // since otherwise the table becomes so expensive to compute that bazel
274
+ // will fail the test with timeouts.
275
+ const LARGEST_TABLE_MUL : usize = if cfg ! ( debug_assertions) { 8 } else { 20 } ;
276
+
151
277
pub fn new ( n : usize , m : usize ) -> Self {
152
278
let scale_range = 1 << CHALLENGE_BITS ;
153
279
let ss = n * m * ( CHUNK_SIZE - 1 ) * ( scale_range - 1 ) ;
154
- let zz = ( 2 * NUM_ZK_REPETITIONS * ss) as isize ;
155
-
156
- let baby_giant = BabyStepGiantStep :: new ( Gt :: generator ( ) , 1 - zz, 2 * zz - 1 ) ;
280
+ let zz = 2 * NUM_ZK_REPETITIONS * ss;
281
+
282
+ let bsgs_lo = 1 - zz as isize ;
283
+ let bsgs_range = 2 * zz - 1 ;
284
+
285
+ let baby_giant = BabyStepGiantStep :: new (
286
+ Gt :: generator ( ) ,
287
+ bsgs_lo,
288
+ bsgs_range,
289
+ Self :: MAX_TABLE_MBYTES ,
290
+ Self :: LARGEST_TABLE_MUL ,
291
+ ) ;
157
292
Self {
158
293
baby_giant,
159
294
scale_range,
0 commit comments