Skip to content

Commit 8d2878c

Browse files
committed
perf(crypto): CRP-2301 CRP-2292 Baby Step Giant Step improvements
1 parent ce506d8 commit 8d2878c

File tree

3 files changed

+247
-52
lines changed

3 files changed

+247
-52
lines changed

rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/benches/dlog.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fn baby_step_giant_step(c: &mut Criterion) {
5454

5555
let rng = &mut reproducible_rng();
5656

57-
let bsgs = BabyStepGiantStep::new(Gt::generator(), 0, 1 << 16);
57+
let bsgs = BabyStepGiantStep::new(Gt::generator(), 0, 1 << 16, 512, 10);
5858

5959
group.bench_function("solve", |b| {
6060
b.iter_batched_ref(

rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/dlog_recovery.rs

Lines changed: 166 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,46 +73,171 @@ impl HonestDealerDlogLookupTable {
7373
}
7474
}
7575

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+
76183
pub struct BabyStepGiantStep {
77184
// Table storing the baby steps
78-
table: std::collections::HashMap<[u8; Gt::BYTES], isize>,
185+
table: BabyStepGiantStepTable,
79186
// Group element `G * -n`, where `G` is the base element used for the discrete log problem.
80187
giant_step: Gt,
81188
// Group element used as an offset to scale down the discrete log in the `0..range`.
82189
offset: Gt,
83190
// Size of the baby steps table
84-
n: isize,
191+
n: usize,
192+
// Number of giant steps to take
193+
giant_steps: usize,
85194
// Integer representing the smallest discrete log in the search space.
86195
lo: isize,
87-
// Size of the search space.
88-
range: isize,
89196
}
90197

91198
impl BabyStepGiantStep {
92199
/// 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+
};
104229

105-
let giant_step = accum.neg();
230+
let (table, giant_step) = BabyStepGiantStepTable::new(base, table_size);
106231

107232
let offset = base * Scalar::from_isize(lo).neg();
108233

109234
Self {
110235
table,
111236
giant_step,
112237
offset,
113-
n,
238+
n: table_size,
239+
giant_steps,
114240
lo,
115-
range,
116241
}
117242
}
118243

@@ -121,18 +246,12 @@ impl BabyStepGiantStep {
121246
///
122247
/// Returns `None` if the discrete logarithm is not in the searched range.
123248
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-
130249
let mut step = tgt + &self.offset;
131250

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));
136255
}
137256
step += &self.giant_step;
138257
}
@@ -148,12 +267,28 @@ pub struct CheatingDealerDlogSolver {
148267
}
149268

150269
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+
151277
pub fn new(n: usize, m: usize) -> Self {
152278
let scale_range = 1 << CHALLENGE_BITS;
153279
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+
);
157292
Self {
158293
baby_giant,
159294
scale_range,

0 commit comments

Comments
 (0)