@@ -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