@@ -485,12 +485,134 @@ impl Sha256 {
485485 }
486486}
487487
488+ /// Computes SHA-256 hash of a fixed-length message.
489+ ///
490+ /// This function creates a subcircuit that computes the SHA-256 hash of a message
491+ /// with a compile-time known length. Unlike the `Sha256` struct which handles
492+ /// variable-length inputs, this function is optimized for fixed-length inputs where
493+ /// the length is known at circuit construction time.
494+ ///
495+ /// # Arguments
496+ /// * `builder` - Circuit builder for constructing constraints
497+ /// * `message` - Input message as 32-bit words (4 bytes per wire) in big-endian format. Each wire
498+ /// must have the high 32 bits set to zero (enforced as a precondition).
499+ /// * `len_bytes` - The fixed length of the message in bytes (known at compile time)
500+ ///
501+ /// # Returns
502+ /// * `[Wire; 8]` - The SHA-256 digest as 8 wires, each containing a 32-bit word in the low 32 bits
503+ /// (high 32 bits are zero) in big-endian order
504+ ///
505+ /// # Panics
506+ /// * If `message.len()` does not equal exactly `len_bytes.div_ceil(4)`
507+ /// * If the message length in bits cannot fit in 32 bits
508+ ///
509+ /// # Example
510+ /// ```rust,ignore
511+ /// use binius_frontend::circuits::sha256::sha256_fixed;
512+ /// use binius_frontend::compiler::CircuitBuilder;
513+ ///
514+ /// let mut builder = CircuitBuilder::new();
515+ ///
516+ /// // Create input wires for a 32-byte message (8 32-bit words)
517+ /// let message: Vec<_> = (0..8).map(|_| builder.add_witness()).collect();
518+ ///
519+ /// // Compute SHA-256 of the 32-byte message
520+ /// let digest = sha256_fixed(&builder, &message, 32);
521+ /// ```
522+ pub fn sha256_fixed ( builder : & CircuitBuilder , message : & [ Wire ] , len_bytes : usize ) -> [ Wire ; 8 ] {
523+ // Validate that message.len() equals exactly len_bytes.div_ceil(4)
524+ assert_eq ! (
525+ message. len( ) ,
526+ len_bytes. div_ceil( 4 ) ,
527+ "message.len() ({}) must equal len_bytes.div_ceil(4) ({})" ,
528+ message. len( ) ,
529+ len_bytes. div_ceil( 4 )
530+ ) ;
531+
532+ // Ensure message length in bits fits in 32 bits
533+ assert ! (
534+ ( len_bytes as u64 )
535+ . checked_mul( 8 )
536+ . is_some_and( |bits| bits <= u32 :: MAX as u64 ) ,
537+ "Message length in bits must fit in 32 bits"
538+ ) ;
539+
540+ // Calculate padding requirements
541+ // SHA-256 requires: message || 0x80 || zeros || 64-bit length field
542+ // The 64-bit length field goes in the last 8 bytes of a block
543+ // We need at least 9 bytes for padding (1 for 0x80 + 8 for length)
544+ let n_blocks = ( len_bytes + 9 ) . div_ceil ( 64 ) ;
545+ let n_padded_words = n_blocks * 16 ; // 16 32-bit words per block
546+
547+ // Create padded message
548+ let mut padded_message = Vec :: with_capacity ( n_padded_words) ;
549+
550+ // Add message words
551+ let n_message_words = len_bytes / 4 ;
552+ let boundary_bytes = len_bytes % 4 ;
553+
554+ // Add complete message words
555+ padded_message. extend_from_slice ( & message[ 0 ..n_message_words] ) ;
556+
557+ // Handle partial word at boundary
558+ if boundary_bytes > 0 {
559+ // The last message word contains partial data
560+ let last_word = message[ n_message_words] ;
561+
562+ // Mask out the unused bytes and add delimiter
563+ let shift_amount = ( 4 - boundary_bytes) * 8 ;
564+ let mask = builder. add_constant ( Word ( ( 0xFFFFFFFFu64 >> shift_amount) << shift_amount) ) ;
565+ let masked = builder. band ( last_word, mask) ;
566+
567+ // Add 0x80 delimiter at the right position
568+ let delimiter_shift = ( 3 - boundary_bytes) * 8 ;
569+ let delimiter = builder. add_constant ( Word ( 0x80u64 << delimiter_shift) ) ;
570+ let boundary_word = builder. bxor ( masked, delimiter) ;
571+
572+ padded_message. push ( boundary_word) ;
573+ } else {
574+ // Message ends at word boundary - delimiter goes in new word
575+ padded_message. push ( builder. add_constant ( Word ( 0x80000000 ) ) ) ;
576+ }
577+
578+ // Fill with zeros until we reach the length field position
579+ let zero = builder. add_constant ( Word :: ZERO ) ;
580+ padded_message. resize ( n_padded_words - 2 , zero) ;
581+
582+ // Add the length field (64 bits total)
583+ padded_message. push ( zero) ; // High 32 bits of length (always 0 for us)
584+ let bitlen = ( len_bytes as u64 ) * 8 ;
585+ padded_message. push ( builder. add_constant ( Word ( bitlen) ) ) ;
586+
587+ // Process compression blocks
588+ let state_out = padded_message. chunks_exact ( 16 ) . enumerate ( ) . fold (
589+ State :: iv ( builder) ,
590+ |state, ( block_idx, block) | {
591+ let block_message: [ Wire ; 16 ] = block. try_into ( ) . unwrap ( ) ;
592+
593+ let compress = Compress :: new (
594+ & builder. subcircuit ( format ! ( "sha256_fixed_compress[{}]" , block_idx) ) ,
595+ state. clone ( ) ,
596+ block_message,
597+ ) ;
598+
599+ compress. state_out
600+ } ,
601+ ) ;
602+
603+ // Return the final state as 8 32-bit words
604+ state_out. 0
605+ }
606+
488607#[ cfg( test) ]
489608mod tests {
609+ use std:: array;
610+
490611 use binius_core:: Word ;
491612 use hex_literal:: hex;
613+ use sha2:: Digest ;
492614
493- use super :: Sha256 ;
615+ use super :: * ;
494616 use crate :: {
495617 compiler:: { self , Wire } ,
496618 constraint_verifier:: verify_constraints,
@@ -906,4 +1028,105 @@ mod tests {
9061028 }
9071029 assert_eq ! ( & extracted_bytes, message) ;
9081030 }
1031+
1032+ // Helper function for sha256_fixed tests
1033+ fn test_sha256_fixed_with_input ( message : & [ u8 ] , expected_bytes : [ u8 ; 32 ] ) {
1034+ let b = CircuitBuilder :: new ( ) ;
1035+
1036+ // Pack message into 32-bit words
1037+ let n_words = message. len ( ) . div_ceil ( 4 ) ;
1038+ let mut message_wires = Vec :: new ( ) ;
1039+
1040+ for word_idx in 0 ..n_words {
1041+ let mut packed = 0u32 ;
1042+ for i in 0 ..4 {
1043+ let byte_idx = word_idx * 4 + i;
1044+ if byte_idx < message. len ( ) {
1045+ packed |= ( message[ byte_idx] as u32 ) << ( 24 - i * 8 ) ;
1046+ }
1047+ }
1048+ message_wires. push ( b. add_constant ( Word ( packed as u64 ) ) ) ;
1049+ }
1050+
1051+ // Create expected digest wires (8 32-bit words)
1052+ let expected_digest_wires = array:: from_fn :: < _ , 8 , _ > ( |_| b. add_inout ( ) ) ;
1053+
1054+ // Compute the digest
1055+ let computed_digest = sha256_fixed ( & b, & message_wires, message. len ( ) ) ;
1056+
1057+ // Assert that computed digest equals expected digest
1058+ for i in 0 ..8 {
1059+ b. assert_eq ( format ! ( "digest[{}]" , i) , computed_digest[ i] , expected_digest_wires[ i] ) ;
1060+ }
1061+
1062+ let circuit = b. build ( ) ;
1063+ let cs = circuit. constraint_system ( ) ;
1064+ let mut w = circuit. new_witness_filler ( ) ;
1065+
1066+ // Populate the expected digest wires
1067+ for i in 0 ..8 {
1068+ let mut word = 0u32 ;
1069+ for j in 0 ..4 {
1070+ word |= ( expected_bytes[ i * 4 + j] as u32 ) << ( 24 - j * 8 ) ;
1071+ }
1072+ w[ expected_digest_wires[ i] ] = Word ( word as u64 ) ;
1073+ }
1074+
1075+ circuit. populate_wire_witness ( & mut w) . unwrap ( ) ;
1076+ verify_constraints ( cs, & w. into_value_vec ( ) ) . unwrap ( ) ;
1077+ }
1078+
1079+ #[ test]
1080+ #[ should_panic( expected = "message.len() (1) must equal len_bytes.div_ceil(4) (2)" ) ]
1081+ fn test_sha256_fixed_with_insufficient_wires ( ) {
1082+ use super :: sha256_fixed;
1083+ let builder = compiler:: CircuitBuilder :: new ( ) ;
1084+
1085+ // Create only 1 wire but claim message is 5 bytes (which needs 2 wires)
1086+ let message_wires: Vec < Wire > = vec ! [ builder. add_witness( ) ] ;
1087+
1088+ // This should panic because message.len() (1) != len_bytes.div_ceil(4) (2)
1089+ sha256_fixed ( & builder, & message_wires, 5 ) ;
1090+ }
1091+
1092+ #[ test]
1093+ fn test_sha256_fixed_various_sizes ( ) {
1094+ use rand:: { Rng , SeedableRng , rngs:: StdRng } ;
1095+
1096+ // Test various message sizes to ensure padding works correctly
1097+ let sizes = vec ! [
1098+ 0 , // empty
1099+ 1 , // single byte
1100+ 3 , // "abc" test vector
1101+ 4 , // exactly one word
1102+ 5 , // just over word boundary
1103+ 31 , // just under half block
1104+ 32 , // exactly half block
1105+ 33 , // just over half block
1106+ 55 , // max single block
1107+ 56 , // forces two blocks
1108+ 63 , // one byte from block boundary
1109+ 64 , // exactly one block
1110+ 65 , // just over one block
1111+ 119 , // max two blocks
1112+ 120 , // forces three blocks
1113+ 128 , // exactly two blocks
1114+ 256 , // exactly four blocks
1115+ ] ;
1116+
1117+ let mut rng = StdRng :: seed_from_u64 ( 0 ) ;
1118+
1119+ for size in sizes {
1120+ // Generate random payload
1121+ let mut message = vec ! [ 0u8 ; size] ;
1122+ rng. fill ( & mut message[ ..] ) ;
1123+
1124+ // Compute expected hash using sha2 crate
1125+ let expected = sha2:: Sha256 :: digest ( & message) ;
1126+ let expected_bytes: [ u8 ; 32 ] = expected. into ( ) ;
1127+
1128+ // Test with our circuit
1129+ test_sha256_fixed_with_input ( & message, expected_bytes) ;
1130+ }
1131+ }
9091132}
0 commit comments