Skip to content

Commit b590580

Browse files
Add malicious oracle tests
1 parent c2b01c9 commit b590580

File tree

1 file changed

+320
-0
lines changed

1 file changed

+320
-0
lines changed

basic_system/src/system_functions/field_ops.rs

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,324 @@ mod tests {
290290
prop_assert_eq!(scalar_default.to_repr(), scalar_oracle.to_repr(), "scalar inverse values should match");
291291
});
292292
}
293+
294+
/// Tests that verify the validation logic catches lying oracles.
295+
/// These tests ensure that incorrect oracle responses are rejected.
296+
mod malicious_oracle_tests {
297+
use super::*;
298+
use oracle_provider::{MemorySource, OracleQueryProcessor};
299+
use proptest::prop_assert;
300+
301+
/// Ways to corrupt oracle responses
302+
enum Corruption {
303+
/// Return all zeros
304+
ReturnZero,
305+
/// Flip the least significant bit of the result
306+
FlipLsb,
307+
/// Add 1 to the result (wrapping)
308+
AddOne,
309+
/// Return a fixed arbitrary value
310+
ReturnArbitrary([u8; 32]),
311+
}
312+
313+
impl Corruption {
314+
fn apply(&self, data: &mut [u8]) {
315+
match self {
316+
Corruption::ReturnZero => data.fill(0),
317+
Corruption::FlipLsb => {
318+
if !data.is_empty() {
319+
data[data.len() - 1] ^= 1;
320+
}
321+
}
322+
Corruption::AddOne => {
323+
// Add 1 with carry propagation (big-endian)
324+
let mut carry = 1u16;
325+
for byte in data.iter_mut().rev() {
326+
let sum = *byte as u16 + carry;
327+
*byte = sum as u8;
328+
carry = sum >> 8;
329+
}
330+
}
331+
Corruption::ReturnArbitrary(val) => {
332+
data.copy_from_slice(val);
333+
}
334+
}
335+
}
336+
}
337+
338+
/// A malicious oracle processor that wraps a correct one and corrupts its output
339+
struct LyingFieldOpsQuery<M: MemorySource> {
340+
inner: callable_oracles::field_hints::NativeFieldOpsQuery<M>,
341+
corruption: Corruption,
342+
/// If set, lie about sqrt existence (flip the boolean)
343+
lie_about_sqrt_existence: bool,
344+
}
345+
346+
impl<M: MemorySource> LyingFieldOpsQuery<M> {
347+
fn new(corruption: Corruption) -> Self {
348+
Self {
349+
inner: callable_oracles::field_hints::NativeFieldOpsQuery::default(),
350+
corruption,
351+
lie_about_sqrt_existence: false,
352+
}
353+
}
354+
355+
fn with_sqrt_existence_lie(mut self) -> Self {
356+
self.lie_about_sqrt_existence = true;
357+
self
358+
}
359+
}
360+
361+
impl<M: MemorySource> OracleQueryProcessor<M> for LyingFieldOpsQuery<M> {
362+
fn supported_query_ids(&self) -> Vec<u32> {
363+
self.inner.supported_query_ids()
364+
}
365+
366+
fn process_buffered_query(
367+
&mut self,
368+
query_id: u32,
369+
query: Vec<usize>,
370+
memory: &M,
371+
) -> Box<dyn ExactSizeIterator<Item = usize> + 'static + Send + Sync> {
372+
// Get the correct response
373+
let correct_iter = self.inner.process_buffered_query(query_id, query, memory);
374+
let correct_response: Vec<usize> = correct_iter.collect();
375+
376+
// Determine if this is a sqrt query (returns Bytes32 + bool) or inverse query (returns Bytes32)
377+
// sqrt response: 4 usize for Bytes32 + 1 usize for bool = 5 usize
378+
// inverse response: 4 usize for Bytes32 = 4 usize
379+
let is_sqrt_query = correct_response.len() == 5;
380+
381+
let mut corrupted = correct_response.clone();
382+
383+
if is_sqrt_query && self.lie_about_sqrt_existence {
384+
// Flip the boolean (last element)
385+
corrupted[4] ^= 1;
386+
} else {
387+
// Corrupt the Bytes32 result (first 4 usize = 32 bytes)
388+
let mut bytes = [0u8; 32];
389+
for (i, &word) in corrupted[..4].iter().enumerate() {
390+
bytes[i * 8..(i + 1) * 8].copy_from_slice(&word.to_le_bytes());
391+
}
392+
self.corruption.apply(&mut bytes);
393+
for (i, chunk) in bytes.chunks(8).enumerate() {
394+
corrupted[i] = usize::from_le_bytes(chunk.try_into().unwrap());
395+
}
396+
}
397+
398+
Box::new(corrupted.into_iter())
399+
}
400+
}
401+
402+
fn create_lying_oracle(
403+
corruption: Corruption,
404+
) -> ZkEENonDeterminismSource<DummyMemorySource> {
405+
let mut oracle = ZkEENonDeterminismSource::<DummyMemorySource>::default();
406+
oracle.add_external_processor(LyingFieldOpsQuery::<DummyMemorySource>::new(corruption));
407+
oracle
408+
}
409+
410+
fn create_sqrt_existence_lying_oracle() -> ZkEENonDeterminismSource<DummyMemorySource> {
411+
let mut oracle = ZkEENonDeterminismSource::<DummyMemorySource>::default();
412+
oracle.add_external_processor(
413+
LyingFieldOpsQuery::<DummyMemorySource>::new(Corruption::ReturnZero)
414+
.with_sqrt_existence_lie(),
415+
);
416+
oracle
417+
}
418+
419+
// A known valid field element for testing (small value, definitely in field)
420+
fn test_field_element() -> FieldElement {
421+
let mut bytes = [0u8; 32];
422+
bytes[31] = 7; // Small non-zero value
423+
FieldElement::from_bytes(&bytes).unwrap()
424+
}
425+
426+
fn test_scalar() -> Scalar {
427+
use crypto::k256::elliptic_curve::scalar::FromUintUnchecked;
428+
let mut bytes = [0u8; 32];
429+
bytes[31] = 7;
430+
Scalar::from_k256_scalar(crypto::k256::Scalar::from_uint_unchecked(
431+
crypto::k256::U256::from_be_slice(&bytes),
432+
))
433+
}
434+
435+
// ============ fe_invert tests ============
436+
437+
#[test]
438+
#[should_panic]
439+
fn test_fe_invert_rejects_zero_answer() {
440+
let mut oracle = create_lying_oracle(Corruption::ReturnZero);
441+
let mut fe = test_field_element();
442+
Secp256k1HooksWithOracle::new(&mut oracle).fe_invert_and_assign(&mut fe);
443+
}
444+
445+
#[test]
446+
#[should_panic]
447+
fn test_fe_invert_rejects_flipped_bit() {
448+
let mut oracle = create_lying_oracle(Corruption::FlipLsb);
449+
let mut fe = test_field_element();
450+
Secp256k1HooksWithOracle::new(&mut oracle).fe_invert_and_assign(&mut fe);
451+
}
452+
453+
#[test]
454+
#[should_panic]
455+
fn test_fe_invert_rejects_off_by_one() {
456+
let mut oracle = create_lying_oracle(Corruption::AddOne);
457+
let mut fe = test_field_element();
458+
Secp256k1HooksWithOracle::new(&mut oracle).fe_invert_and_assign(&mut fe);
459+
}
460+
461+
#[test]
462+
#[should_panic]
463+
fn test_fe_invert_rejects_arbitrary_value() {
464+
let arbitrary = [0x42u8; 32];
465+
let mut oracle = create_lying_oracle(Corruption::ReturnArbitrary(arbitrary));
466+
let mut fe = test_field_element();
467+
Secp256k1HooksWithOracle::new(&mut oracle).fe_invert_and_assign(&mut fe);
468+
}
469+
470+
// ============ fe_sqrt tests ============
471+
472+
#[test]
473+
#[should_panic]
474+
fn test_fe_sqrt_rejects_wrong_sqrt_value() {
475+
let mut oracle = create_lying_oracle(Corruption::FlipLsb);
476+
let mut fe = test_field_element();
477+
Secp256k1HooksWithOracle::new(&mut oracle).fe_sqrt_and_assign(&mut fe);
478+
}
479+
480+
#[test]
481+
#[should_panic]
482+
fn test_fe_sqrt_rejects_zero_answer() {
483+
let mut oracle = create_lying_oracle(Corruption::ReturnZero);
484+
let mut fe = test_field_element();
485+
Secp256k1HooksWithOracle::new(&mut oracle).fe_sqrt_and_assign(&mut fe);
486+
}
487+
488+
#[test]
489+
#[should_panic]
490+
fn test_fe_sqrt_rejects_lie_about_existence() {
491+
// This test uses an oracle that returns the correct sqrt value but lies
492+
// about whether a sqrt exists (flips the boolean)
493+
let mut oracle = create_sqrt_existence_lying_oracle();
494+
let mut fe = test_field_element();
495+
Secp256k1HooksWithOracle::new(&mut oracle).fe_sqrt_and_assign(&mut fe);
496+
}
497+
498+
// ============ scalar_invert tests ============
499+
500+
#[test]
501+
#[should_panic]
502+
fn test_scalar_invert_rejects_zero_answer() {
503+
let mut oracle = create_lying_oracle(Corruption::ReturnZero);
504+
let mut scalar = test_scalar();
505+
Secp256k1HooksWithOracle::new(&mut oracle).scalar_invert_and_assign(&mut scalar);
506+
}
507+
508+
#[test]
509+
#[should_panic]
510+
fn test_scalar_invert_rejects_flipped_bit() {
511+
let mut oracle = create_lying_oracle(Corruption::FlipLsb);
512+
let mut scalar = test_scalar();
513+
Secp256k1HooksWithOracle::new(&mut oracle).scalar_invert_and_assign(&mut scalar);
514+
}
515+
516+
#[test]
517+
#[should_panic]
518+
fn test_scalar_invert_rejects_off_by_one() {
519+
let mut oracle = create_lying_oracle(Corruption::AddOne);
520+
let mut scalar = test_scalar();
521+
Secp256k1HooksWithOracle::new(&mut oracle).scalar_invert_and_assign(&mut scalar);
522+
}
523+
524+
#[test]
525+
#[should_panic]
526+
fn test_scalar_invert_rejects_arbitrary_value() {
527+
let arbitrary = [0x42u8; 32];
528+
let mut oracle = create_lying_oracle(Corruption::ReturnArbitrary(arbitrary));
529+
let mut scalar = test_scalar();
530+
Secp256k1HooksWithOracle::new(&mut oracle).scalar_invert_and_assign(&mut scalar);
531+
}
532+
533+
// ============ Proptest: random corruptions should be rejected ============
534+
535+
#[test]
536+
fn test_fe_invert_rejects_random_corruptions() {
537+
proptest!(|(bytes: [u8; 32], corruption_bytes: [u8; 32])| {
538+
let Some(fe) = FieldElement::from_bytes(&bytes) else {
539+
return Ok(());
540+
};
541+
if fe.normalizes_to_zero() {
542+
return Ok(());
543+
}
544+
545+
// Get the correct inverse first
546+
let mut correct_fe = fe;
547+
let mut correct_oracle = create_oracle_with_field_ops();
548+
Secp256k1HooksWithOracle::new(&mut correct_oracle).fe_invert_and_assign(&mut correct_fe);
549+
let correct_inverse = correct_fe.to_bytes();
550+
551+
// Skip if random corruption happens to equal the correct answer
552+
if corruption_bytes == *correct_inverse {
553+
return Ok(());
554+
}
555+
556+
// Now try with the corrupted oracle
557+
let mut lying_oracle = create_lying_oracle(Corruption::ReturnArbitrary(corruption_bytes));
558+
let mut test_fe = fe;
559+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
560+
Secp256k1HooksWithOracle::new(&mut lying_oracle).fe_invert_and_assign(&mut test_fe);
561+
}));
562+
563+
// The validation should have caught the lie (panicked)
564+
prop_assert!(result.is_err(), "Oracle lie was not detected for input {:?}", bytes);
565+
});
566+
}
567+
568+
#[test]
569+
fn test_scalar_invert_rejects_random_corruptions() {
570+
proptest!(|(bytes: [u8; 32], corruption_bytes: [u8; 32])| {
571+
use crypto::k256::elliptic_curve::scalar::FromUintUnchecked;
572+
use crypto::k256::elliptic_curve::Curve;
573+
use crypto::k256::U256;
574+
575+
let val = U256::from_be_slice(&bytes);
576+
if val >= crypto::k256::Secp256k1::ORDER || val == U256::ZERO {
577+
return Ok(());
578+
}
579+
580+
let scalar = Scalar::from_k256_scalar(
581+
crypto::k256::Scalar::from_uint_unchecked(val)
582+
);
583+
584+
// Get the correct inverse first
585+
let mut correct_scalar = scalar;
586+
let mut correct_oracle = create_oracle_with_field_ops();
587+
Secp256k1HooksWithOracle::new(&mut correct_oracle).scalar_invert_and_assign(&mut correct_scalar);
588+
let correct_inverse = correct_scalar.to_repr();
589+
590+
// Skip if random corruption happens to equal the correct answer
591+
if corruption_bytes == *correct_inverse {
592+
return Ok(());
593+
}
594+
595+
// Also skip if corruption_bytes >= ORDER (would fail earlier validation)
596+
let corruption_val = U256::from_be_slice(&corruption_bytes);
597+
if corruption_val >= crypto::k256::Secp256k1::ORDER {
598+
return Ok(());
599+
}
600+
601+
// Now try with the corrupted oracle
602+
let mut lying_oracle = create_lying_oracle(Corruption::ReturnArbitrary(corruption_bytes));
603+
let mut test_scalar = scalar;
604+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
605+
Secp256k1HooksWithOracle::new(&mut lying_oracle).scalar_invert_and_assign(&mut test_scalar);
606+
}));
607+
608+
// The validation should have caught the lie (panicked)
609+
prop_assert!(result.is_err(), "Oracle lie was not detected for input {:?}", bytes);
610+
});
611+
}
612+
}
293613
}

0 commit comments

Comments
 (0)