Skip to content

Commit 0d289ae

Browse files
authored
test(coprocessor): strengthen error and random bounded test assertions (#2029)
Error tests now assert the specific error message instead of only checking is_error == true. The bounded random test now generates two samples per type with different seeds and asserts they differ, catching any constant-output RNG implementation including always-zero. Closes zama-ai/fhevm-internal#1077
1 parent 624dc2f commit 0d289ae

File tree

2 files changed

+93
-17
lines changed

2 files changed

+93
-17
lines changed

coprocessor/fhevm-engine/tfhe-worker/src/tests/errors.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ async fn test_coprocessor_input_errors() -> Result<(), Box<dyn std::error::Error
5353
is_error,
5454
"expected unknown operation to fail, last_error_message={msg:?}"
5555
);
56+
let error_msg = msg.as_deref().unwrap_or("");
57+
assert!(
58+
error_msg.contains("Unknown fhe operation"),
59+
"expected 'Unknown fhe operation' error, got: {error_msg}"
60+
);
5661
Ok(())
5762
}
5863

@@ -100,6 +105,11 @@ async fn test_coprocessor_computation_errors() -> Result<(), Box<dyn std::error:
100105
is_error,
101106
"expected FheSub on mismatched types to fail, last_error_message={msg:?}"
102107
);
108+
let error_msg = msg.as_deref().unwrap_or("");
109+
assert!(
110+
error_msg.contains("UnsupportedFheTypes"),
111+
"expected UnsupportedFheTypes error, got: {error_msg}"
112+
);
103113
Ok(())
104114
}
105115

@@ -146,6 +156,11 @@ async fn test_type_mismatch_error() -> Result<(), Box<dyn std::error::Error>> {
146156
is_error,
147157
"expected FheAdd on mismatched types to fail, last_error_message={msg:?}"
148158
);
159+
let error_msg = msg.as_deref().unwrap_or("");
160+
assert!(
161+
error_msg.contains("UnsupportedFheTypes"),
162+
"expected UnsupportedFheTypes error, got: {error_msg}"
163+
);
149164
Ok(())
150165
}
151166

@@ -190,6 +205,11 @@ async fn test_binary_boolean_inputs_error() -> Result<(), Box<dyn std::error::Er
190205
is_error,
191206
"expected FheAdd on bool inputs to fail, last_error_message={msg:?}"
192207
);
208+
let error_msg = msg.as_deref().unwrap_or("");
209+
assert!(
210+
error_msg.contains("UnsupportedFheTypes"),
211+
"expected UnsupportedFheTypes error, got: {error_msg}"
212+
);
193213
Ok(())
194214
}
195215

@@ -230,5 +250,10 @@ async fn test_unary_boolean_inputs_error() -> Result<(), Box<dyn std::error::Err
230250
is_error,
231251
"expected FheNeg on bool input to fail, last_error_message={msg:?}"
232252
);
253+
let error_msg = msg.as_deref().unwrap_or("");
254+
assert!(
255+
error_msg.contains("UnsupportedFheTypes"),
256+
"expected UnsupportedFheTypes error, got: {error_msg}"
257+
);
233258
Ok(())
234259
}

coprocessor/fhevm-engine/tfhe-worker/src/tests/random.rs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async fn test_fhe_random_basic() -> Result<(), Box<dyn std::error::Error>> {
9595
TfheContractEvents::FheRand(TfheContract::FheRand {
9696
caller: zero_address(),
9797
randType: to_ty(rand_type),
98-
seed: FixedBytes::from([1_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
98+
seed: FixedBytes::from([42_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
9999
result: output3,
100100
}),
101101
true,
@@ -123,18 +123,26 @@ async fn test_fhe_random_basic() -> Result<(), Box<dyn std::error::Error>> {
123123
first.value, second.value,
124124
"random generation must be deterministic for same seed"
125125
);
126+
// FheBool::generate_oblivious_pseudo_random produces the same
127+
// plaintext for all seeds with a given key, so we can only check
128+
// seed-variance for non-bool types.
126129
if *rand_type != 0 {
127130
assert_ne!(
128131
first.value, third.value,
129-
"random generation must change when seed changes"
132+
"type {rand_type}: random generation must change when seed changes"
130133
);
131134
}
132135
}
133136

134137
Ok(())
135138
}
136139

137-
/// Verifies FheRandBounded produces values within the requested bounds.
140+
/// Verifies FheRandBounded produces values within the requested bounds
141+
/// and that different seeds yield different results for non-bool types
142+
/// (rejecting a constant-output implementation, e.g. one that always
143+
/// returns zero). Bool is excluded from seed-variance checks because
144+
/// `FheBool::generate_oblivious_pseudo_random` produces the same
145+
/// plaintext for all seeds with a given key.
138146
///
139147
/// Uses per-type bounds that match the old gRPC test to avoid edge cases
140148
/// (e.g. upper_bound=1 produces 0 random bits, which behaves differently
@@ -172,7 +180,8 @@ async fn test_fhe_random_bounded() -> Result<(), Box<dyn std::error::Error>> {
172180
let tx_id = next_handle();
173181
let mut tx = harness.listener_db.new_transaction().await?;
174182

175-
let output = next_handle();
183+
// First sample with seed [1,0,...,0]
184+
let output1 = next_handle();
176185
insert_event(
177186
&harness.listener_db,
178187
&mut tx,
@@ -182,46 +191,88 @@ async fn test_fhe_random_bounded() -> Result<(), Box<dyn std::error::Error>> {
182191
upperBound: as_scalar_uint(&bound),
183192
randType: to_ty(rand_type),
184193
seed: FixedBytes::from([1_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
185-
result: output,
194+
result: output1,
186195
}),
187196
true,
188197
)
189198
.await?;
190-
allow_handle(&harness.listener_db, &mut tx, &output).await?;
199+
allow_handle(&harness.listener_db, &mut tx, &output1).await?;
200+
201+
// Second sample with a different seed
202+
let output2 = next_handle();
203+
insert_event(
204+
&harness.listener_db,
205+
&mut tx,
206+
tx_id,
207+
TfheContractEvents::FheRandBounded(TfheContract::FheRandBounded {
208+
caller: zero_address(),
209+
upperBound: as_scalar_uint(&bound),
210+
randType: to_ty(rand_type),
211+
seed: FixedBytes::from([7_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
212+
result: output2,
213+
}),
214+
true,
215+
)
216+
.await?;
217+
allow_handle(&harness.listener_db, &mut tx, &output2).await?;
191218
tx.commit().await?;
192219

193220
rand_types.push(rand_type);
194221
bounds.push(bound);
195-
handles.push(output);
222+
handles.extend([output1, output2]);
196223
}
197224

198225
wait_until_computed(&harness.app).await?;
199226
let decrypted = decrypt_handles(&harness.pool, &handles).await?;
200227

201228
for (idx, rand_type) in rand_types.iter().enumerate() {
202-
let result = &decrypted[idx];
203-
assert_eq!(result.output_type, *rand_type as i16);
229+
let base = idx * 2;
230+
let result1 = &decrypted[base];
231+
let result2 = &decrypted[base + 1];
232+
assert_eq!(result1.output_type, *rand_type as i16);
233+
assert_eq!(result2.output_type, *rand_type as i16);
204234

205-
// Bool is special: only valid values are 0/1, and bound=2 means [0,2).
206235
if *rand_type == 0 {
236+
// FheBool::generate_oblivious_pseudo_random produces the same
237+
// plaintext for all seeds with a given key, so we can only
238+
// validate the value domain, not seed-variance.
239+
assert!(
240+
result1.value == "true" || result1.value == "false",
241+
"bool rand_bounded should be true or false, got: {}",
242+
result1.value
243+
);
207244
assert!(
208-
result.value == "true" || result.value == "false",
245+
result2.value == "true" || result2.value == "false",
209246
"bool rand_bounded should be true or false, got: {}",
210-
result.value
247+
result2.value
211248
);
212249
continue;
213250
}
214251

215-
let result_num = BigInt::from_str(&result.value)?;
252+
let result1_num = BigInt::from_str(&result1.value)?;
253+
let result2_num = BigInt::from_str(&result2.value)?;
216254
assert!(
217-
result_num >= BigInt::from(0_u8),
218-
"type {rand_type}: rand_bounded result should be >= 0, got {result_num}"
255+
result1_num >= BigInt::from(0_u8),
256+
"type {rand_type}: rand_bounded result should be >= 0, got {result1_num}"
219257
);
220258
assert!(
221-
result_num < bounds[idx],
222-
"type {rand_type}: rand_bounded result {result_num} should be < bound {}",
259+
result1_num < bounds[idx],
260+
"type {rand_type}: rand_bounded result {result1_num} should be < bound {}",
223261
bounds[idx]
224262
);
263+
assert!(
264+
result2_num >= BigInt::from(0_u8),
265+
"type {rand_type}: rand_bounded result should be >= 0, got {result2_num}"
266+
);
267+
assert!(
268+
result2_num < bounds[idx],
269+
"type {rand_type}: rand_bounded result {result2_num} should be < bound {}",
270+
bounds[idx]
271+
);
272+
assert_ne!(
273+
result1_num, result2_num,
274+
"type {rand_type}: bounded random must vary with seed"
275+
);
225276
}
226277

227278
Ok(())

0 commit comments

Comments
 (0)