@@ -8,13 +8,16 @@ use thiserror::Error;
88#[ cfg( test) ]
99use crate :: entities:: Epoch ;
1010
11+ #[ cfg( feature = "future_snark" ) ]
12+ use crate :: crypto_helper:: ProtocolAggregateVerificationKeyForSnark ;
13+
1114/// Error returned by [ProtocolMessage::check_rigid_integrity] when a rigid-segment value
1215/// in a [ProtocolMessage] does not match the fixed-size SNARK-friendly slot it must fill.
1316#[ cfg( feature = "future_snark" ) ]
1417#[ derive( Debug , Error , PartialEq , Eq ) ]
1518pub enum RigidProtocolMessageIntegrityError {
1619 /// The decoded value of a fixed-size rigid field does not match the expected byte length.
17- #[ error( "rigid `{field}` value must decode to exactly {expected} bytes (got {actual})" ) ]
20+ #[ error( "Rigid `{field}` value must decode to exactly {expected} bytes (got {actual})" ) ]
1821 UnexpectedFieldLength {
1922 /// Name of the rigid field whose decoded bytes do not match the expected length.
2023 field : & ' static str ,
@@ -26,59 +29,70 @@ pub enum RigidProtocolMessageIntegrityError {
2629
2730 /// The required `current_epoch` entry is missing from the protocol message.
2831 #[ error(
29- "rigid `current_epoch` value is required but no entry was found in the protocol message"
32+ "Rigid `current_epoch` value is required but no entry was found in the protocol message"
3033 ) ]
3134 MissingCurrentEpoch ,
3235
3336 /// The `current_epoch` value cannot be parsed as a base-10 unsigned 64-bit integer.
34- #[ error( "rigid `current_epoch` value must be a base-10 unsigned 64-bit integer: {0}" ) ]
37+ #[ error( "Rigid `current_epoch` value must be a base-10 unsigned 64-bit integer: {0}" ) ]
3538 InvalidCurrentEpoch ( String ) ,
3639
3740 /// The required `next_aggregate_verification_key` entry is missing from the protocol message.
3841 #[ error(
39- "rigid `next_aggregate_verification_key` value is required but no entry was found in the protocol message"
42+ "Rigid `next_aggregate_verification_key` value is required but no entry was found in the protocol message"
4043 ) ]
4144 MissingNextSnarkAggregateVerificationKey ,
4245
4346 /// The required `next_protocol_parameters` entry is missing from the protocol message.
4447 #[ error(
45- "rigid `next_protocol_parameters` value is required but no entry was found in the protocol message"
48+ "Rigid `next_protocol_parameters` value is required but no entry was found in the protocol message"
4649 ) ]
4750 MissingNextProtocolParameters ,
51+
52+ /// The protocol parameters value cannot be decoded.
53+ #[ error( "Rigid `next_protocol_parameters` value cannot be decoded: {0}" ) ]
54+ InvalidProtocolParameters ( String ) ,
55+
56+ /// The SNARK aggregate verification key cannot be decoded.
57+ #[ error( "Rigid `next_aggregate_verification_key` value cannot be decoded: {0}" ) ]
58+ InvalidSnarkAggregateVerificationKey ( String ) ,
59+
60+ /// The decoded SNARK aggregate verification key cannot be projected into the rigid slot
61+ /// (e.g. the embedded Merkle root does not have the expected 32-byte width).
62+ #[ error(
63+ "Rigid `next_aggregate_verification_key` value cannot be projected into the rigid slot: {0}"
64+ ) ]
65+ UnprojectableSnarkAggregateVerificationKey ( String ) ,
4866}
4967
50- /// Decode the wire SNARK aggregate verification key value (CBOR-prefixed AVK encoding produced
51- /// by `ProtocolKey::new(snark_avk).to_bytes_hex()`) and project it into the canonical
52- /// [RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES]-byte rigid-slot layout.
53- ///
54- /// Producers keep emitting the full AVK CBOR encoding so the certificate verifier can still
55- /// deserialize the previous-cert announcement back to a `ProtocolAggregateVerificationKeyForSnark`
56- /// for full-equality comparison. The rigid hash assembler (and the integrity check) decode that
57- /// wire value and project the AVK into the fixed-size rigid slot via
68+ /// Decode the SNARK AVK (hex of CBOR-prefixed bytes from `ProtocolKey::to_bytes_hex`) and
69+ /// project it into the [RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES]-byte rigid slot via
5870/// `AggregateVerificationKeyForSnark::to_rigid_slot_bytes`.
5971#[ cfg( feature = "future_snark" ) ]
6072fn decode_snark_avk_to_rigid_slot_bytes (
6173 value : & str ,
6274) -> Result < [ u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] , RigidProtocolMessageIntegrityError > {
63- let snark_avk = crate :: crypto_helper:: ProtocolAggregateVerificationKeyForSnark :: try_from ( value)
64- . map_err ( |err| {
65- RigidProtocolMessageIntegrityError :: InvalidSnarkAggregateVerificationKey (
66- err. to_string ( ) ,
67- )
68- } ) ?;
69- snark_avk. to_rigid_slot_bytes ( ) . map_err ( |err| {
75+ let snark_avk = ProtocolAggregateVerificationKeyForSnark :: try_from ( value) . map_err ( |err| {
7076 RigidProtocolMessageIntegrityError :: InvalidSnarkAggregateVerificationKey ( err. to_string ( ) )
77+ } ) ?;
78+ snark_avk. to_rigid_slot_bytes ( ) . map_err ( |err| {
79+ RigidProtocolMessageIntegrityError :: UnprojectableSnarkAggregateVerificationKey (
80+ err. to_string ( ) ,
81+ )
7182 } )
7283}
7384
74- /// Decode the wire protocol parameters value (hex-encoded fixed-size buffer, falling back to
75- /// raw UTF-8 bytes when hex decoding fails) and project it into the
85+ /// Decode the protocol parameters value (hex-encoded
86+ /// [ProtocolParameters::compute_hash](crate::entities::ProtocolParameters::compute_hash) output,
87+ /// a SHA-256 digest of the protocol parameters) and project it into the
7688/// [RIGID_NEXT_PROTOCOL_PARAMETERS_BYTES]-byte rigid slot.
7789#[ cfg( feature = "future_snark" ) ]
7890fn decode_protocol_parameters_to_rigid_slot_bytes (
7991 value : & str ,
8092) -> Result < [ u8 ; RIGID_NEXT_PROTOCOL_PARAMETERS_BYTES ] , RigidProtocolMessageIntegrityError > {
81- let bytes = hex:: decode ( value) . unwrap_or_else ( |_| value. as_bytes ( ) . to_vec ( ) ) ;
93+ let bytes = hex:: decode ( value) . map_err ( |err| {
94+ RigidProtocolMessageIntegrityError :: InvalidProtocolParameters ( err. to_string ( ) )
95+ } ) ?;
8296 bytes. as_slice ( ) . try_into ( ) . map_err ( |_| {
8397 RigidProtocolMessageIntegrityError :: UnexpectedFieldLength {
8498 field : "next_protocol_parameters" ,
@@ -156,7 +170,6 @@ pub enum ProtocolMessageHashScheme {
156170 Legacy ,
157171
158172 /// Lagrange SNARK-friendly hash scheme.
159- #[ cfg( feature = "future_snark" ) ]
160173 #[ serde( rename = "rigid" ) ]
161174 Rigid ,
162175}
@@ -308,18 +321,27 @@ impl ProtocolMessage {
308321 self . message_parts . get ( key)
309322 }
310323
311- /// Return `true` if the protocol message uses the rigid hash scheme.
324+ /// Return `true` if the protocol message uses the [ProtocolMessageHashScheme::Rigid] hash
325+ /// scheme.
312326 pub fn is_rigid ( & self ) -> bool {
313- !matches ! ( self . hash_scheme, ProtocolMessageHashScheme :: Legacy )
327+ if cfg ! ( feature = "future_snark" ) {
328+ self . hash_scheme == ProtocolMessageHashScheme :: Rigid
329+ } else {
330+ false
331+ }
314332 }
315333
316334 /// Compute the hex-encoded SHA-256 hash of the protocol message, dispatching over
317- /// [ProtocolMessage::hash_scheme].
335+ /// [ProtocolMessage::hash_scheme]. The rigid scheme requires `future_snark`; without it,
336+ /// rigid messages fall back to the legacy hash (signals misconfiguration via signature
337+ /// mismatch downstream).
318338 pub fn compute_hash ( & self ) -> String {
319339 match self . hash_scheme {
320340 ProtocolMessageHashScheme :: Legacy => self . compute_legacy_hash ( ) ,
321341 #[ cfg( feature = "future_snark" ) ]
322342 ProtocolMessageHashScheme :: Rigid => self . compute_rigid_hash ( ) ,
343+ #[ cfg( not( feature = "future_snark" ) ) ]
344+ ProtocolMessageHashScheme :: Rigid => self . compute_legacy_hash ( ) ,
323345 }
324346 }
325347
@@ -391,14 +413,7 @@ impl ProtocolMessage {
391413 . message_parts
392414 . get ( & ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey )
393415 . ok_or ( RigidProtocolMessageIntegrityError :: MissingNextSnarkAggregateVerificationKey ) ?;
394- let snark_avk_bytes = legacy_value_to_bytes ( snark_avk) ;
395- if snark_avk_bytes. len ( ) != RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES {
396- return Err ( RigidProtocolMessageIntegrityError :: UnexpectedFieldLength {
397- field : "next_aggregate_verification_key" ,
398- expected : RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ,
399- actual : snark_avk_bytes. len ( ) ,
400- } ) ;
401- }
416+ decode_snark_avk_to_rigid_slot_bytes ( snark_avk) ?;
402417
403418 let protocol_parameters = self
404419 . message_parts
@@ -424,31 +439,19 @@ impl ProtocolMessage {
424439 self . clone ( ) . stripped_for_rigid_digest ( ) . compute_legacy_digest_bytes ( )
425440 }
426441
427- /// Project the [ProtocolMessagePartKey::NextSnarkAggregateVerificationKey] entry into the
428- /// fixed-size [RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES] rigid slot.
429- ///
430- /// Missing entries collapse to a zero-padded buffer and oversized decoded values are
431- /// truncated to the slot width; both cases are accepted silently here. Producers running
432- /// [ProtocolMessage::check_rigid_integrity] reject these as
433- /// [RigidProtocolMessageIntegrityError::MissingNextSnarkAggregateVerificationKey] and
434- /// [RigidProtocolMessageIntegrityError::UnexpectedFieldLength] at construction time. If the
435- /// strict check is bypassed, the verifier recomputes a preimage that does not match the
436- /// signed bytes and the signature-vs-message check fails as a `match_message` mismatch
437- /// instead of a typed error.
442+ /// Decode the host SNARK AVK value (hex of CBOR-prefixed bytes from
443+ /// `ProtocolKey::to_bytes_hex`) and project it into the rigid slot via
444+ /// `AggregateVerificationKeyForSnark::to_rigid_slot_bytes`. Missing or undecodable entries
445+ /// silently collapse to zeros; [ProtocolMessage::check_rigid_integrity] surfaces the typed
446+ /// error.
438447 #[ cfg( feature = "future_snark" ) ]
439448 fn rigid_next_aggregate_verification_key_field (
440449 & self ,
441450 ) -> [ u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] {
442- let mut buffer = [ 0u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ;
443- if let Some ( value) = self
444- . message_parts
451+ self . message_parts
445452 . get ( & ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey )
446- {
447- let bytes = legacy_value_to_bytes ( value) ;
448- let length = bytes. len ( ) . min ( RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ) ;
449- buffer[ ..length] . copy_from_slice ( & bytes[ ..length] ) ;
450- }
451- buffer
453+ . and_then ( |value| decode_snark_avk_to_rigid_slot_bytes ( value) . ok ( ) )
454+ . unwrap_or ( [ 0u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] )
452455 }
453456
454457 /// Project the [ProtocolMessagePartKey::NextProtocolParameters] entry into the rigid slot.
@@ -717,6 +720,20 @@ mod tests {
717720 protocol_message
718721 }
719722
723+ /// Build a syntactically valid SNARK aggregate verification key wire value embedding the
724+ /// given 32-byte Merkle root and total stake.
725+ ///
726+ /// Uses the legacy AVK byte layout (`merkle_root || total_stake_be_u64`) which the
727+ /// `AggregateVerificationKeyForSnark::from_bytes` decoder accepts as a fallback when the
728+ /// CBOR version prefix is absent.
729+ #[ cfg( feature = "future_snark" ) ]
730+ fn build_snark_avk_wire_value_for_test ( merkle_root : [ u8 ; 32 ] , total_stake : u64 ) -> String {
731+ let mut bytes = Vec :: with_capacity ( 40 ) ;
732+ bytes. extend_from_slice ( & merkle_root) ;
733+ bytes. extend_from_slice ( & total_stake. to_be_bytes ( ) ) ;
734+ hex:: encode ( bytes)
735+ }
736+
720737 #[ cfg( feature = "future_snark" ) ]
721738 fn build_rigid_protocol_message_reference ( ) -> ProtocolMessage {
722739 let mut message = ProtocolMessage :: new_rigid ( ) ;
@@ -730,7 +747,7 @@ mod tests {
730747 ) ;
731748 message. set_message_part (
732749 ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey ,
733- hex :: encode ( [ 0xCCu8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ) ,
750+ build_snark_avk_wire_value_for_test ( [ 0xCCu8 ; 32 ] , 0 ) ,
734751 ) ;
735752 message. set_message_part (
736753 ProtocolMessagePartKey :: NextProtocolParameters ,
@@ -923,16 +940,21 @@ mod tests {
923940 #[ test]
924941 fn rigid_preimage_sources_aggregate_verification_key_segment_from_snark_avk_value ( ) {
925942 let mut message = ProtocolMessage :: new_rigid ( ) ;
926- let snark_avk = [ 0xCDu8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ;
943+ let merkle_root = [ 0xCDu8 ; 32 ] ;
944+ let total_stake = 17u64 ;
927945 message. set_message_part (
928946 ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey ,
929- hex :: encode ( snark_avk ) ,
947+ build_snark_avk_wire_value_for_test ( merkle_root , total_stake ) ,
930948 ) ;
931949
950+ let mut expected = [ 0u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ;
951+ expected[ 0 ..32 ] . copy_from_slice ( & merkle_root) ;
952+ expected[ 36 ..44 ] . copy_from_slice ( & total_stake. to_le_bytes ( ) ) ;
953+
932954 assert_eq ! (
933955 message. rigid_next_aggregate_verification_key_field( ) ,
934- snark_avk ,
935- "the rigid AVK segment must be the raw bytes of the SNARK aggregate verification key "
956+ expected ,
957+ "the rigid AVK segment must be the canonical 44-byte projection of the SNARK AVK "
936958 ) ;
937959 }
938960
@@ -1144,7 +1166,8 @@ mod tests {
11441166 #[ cfg( feature = "future_snark" ) ]
11451167 #[ test]
11461168 fn rigid_preimage_is_byte_identical_to_a_hand_built_labeled_concatenation ( ) {
1147- let snark_avk_bytes = [ 5u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ;
1169+ let snark_avk_root = [ 0x05u8 ; 32 ] ;
1170+ let snark_avk_total_stake = 17u64 ;
11481171 let protocol_params_bytes = [ 3u8 ; RIGID_NEXT_PROTOCOL_PARAMETERS_BYTES ] ;
11491172 let epoch_value = 12345u64 ;
11501173
@@ -1155,7 +1178,7 @@ mod tests {
11551178 ) ;
11561179 rigid. set_message_part (
11571180 ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey ,
1158- hex :: encode ( snark_avk_bytes ) ,
1181+ build_snark_avk_wire_value_for_test ( snark_avk_root , snark_avk_total_stake ) ,
11591182 ) ;
11601183 rigid. set_message_part (
11611184 ProtocolMessagePartKey :: NextProtocolParameters ,
@@ -1166,11 +1189,15 @@ mod tests {
11661189 epoch_value. to_string ( ) ,
11671190 ) ;
11681191
1192+ let mut snark_avk_slot = [ 0u8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ] ;
1193+ snark_avk_slot[ 0 ..32 ] . copy_from_slice ( & snark_avk_root) ;
1194+ snark_avk_slot[ 36 ..44 ] . copy_from_slice ( & snark_avk_total_stake. to_le_bytes ( ) ) ;
1195+
11691196 let mut expected = Vec :: new ( ) ;
11701197 expected. extend_from_slice ( b"digest" ) ;
11711198 expected. extend_from_slice ( & rigid. rigid_digest_field ( ) ) ;
11721199 expected. extend_from_slice ( b"next_aggregate_verification_key" ) ;
1173- expected. extend_from_slice ( & snark_avk_bytes ) ;
1200+ expected. extend_from_slice ( & snark_avk_slot ) ;
11741201 expected. extend_from_slice ( b"next_protocol_parameters" ) ;
11751202 expected. extend_from_slice ( & protocol_params_bytes) ;
11761203 expected. extend_from_slice ( b"current_epoch" ) ;
@@ -1179,7 +1206,7 @@ mod tests {
11791206 assert_eq ! (
11801207 expected,
11811208 rigid. rigid_preimage( ) ,
1182- "the rigid preimage must be the byte-identical concatenation of each ASCII label followed by its raw value bytes"
1209+ "the rigid preimage must be the byte-identical concatenation of each ASCII label followed by the rigid-slot value bytes"
11831210 ) ;
11841211 }
11851212
@@ -1242,24 +1269,48 @@ mod tests {
12421269
12431270 #[ cfg( feature = "future_snark" ) ]
12441271 #[ test]
1245- fn check_rigid_integrity_fails_when_next_snark_avk_decodes_to_unexpected_length ( ) {
1272+ fn check_rigid_integrity_fails_when_next_snark_avk_cannot_be_deserialized ( ) {
12461273 let mut rigid = build_rigid_protocol_message_reference ( ) ;
12471274 rigid. set_message_part (
12481275 ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey ,
1249- hex :: encode ( [ 0xCCu8 ; RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES - 1 ] ) ,
1276+ "not-a-valid-snark-avk-encoding" . to_string ( ) ,
12501277 ) ;
12511278
12521279 let error = rigid
12531280 . check_rigid_integrity ( )
1254- . expect_err ( "ill-formed rigid SNARK AVK entry must surface an error" ) ;
1281+ . expect_err ( "undecodable rigid SNARK AVK entry must surface an error" ) ;
12551282
1256- assert_eq ! (
1257- error,
1258- RigidProtocolMessageIntegrityError :: UnexpectedFieldLength {
1259- field: "next_aggregate_verification_key" ,
1260- expected: RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES ,
1261- actual: RIGID_NEXT_AGGREGATE_VERIFICATION_KEY_BYTES - 1 ,
1262- }
1283+ assert ! (
1284+ matches!(
1285+ error,
1286+ RigidProtocolMessageIntegrityError :: InvalidSnarkAggregateVerificationKey ( _)
1287+ ) ,
1288+ "unexpected error variant: {error:?}"
1289+ ) ;
1290+ }
1291+
1292+ #[ cfg( feature = "future_snark" ) ]
1293+ #[ test]
1294+ fn check_rigid_integrity_fails_when_decoded_next_snark_avk_does_not_fit_the_rigid_slot ( ) {
1295+ let mut rigid = build_rigid_protocol_message_reference ( ) ;
1296+ let mut undersized = Vec :: with_capacity ( 24 ) ;
1297+ undersized. extend_from_slice ( & [ 0xAAu8 ; 16 ] ) ;
1298+ undersized. extend_from_slice ( & 0u64 . to_be_bytes ( ) ) ;
1299+ rigid. set_message_part (
1300+ ProtocolMessagePartKey :: NextSnarkAggregateVerificationKey ,
1301+ hex:: encode ( undersized) ,
1302+ ) ;
1303+
1304+ let error = rigid
1305+ . check_rigid_integrity ( )
1306+ . expect_err ( "unprojectable rigid SNARK AVK entry must surface an error" ) ;
1307+
1308+ assert ! (
1309+ matches!(
1310+ error,
1311+ RigidProtocolMessageIntegrityError :: UnprojectableSnarkAggregateVerificationKey ( _)
1312+ ) ,
1313+ "unexpected error variant: {error:?}"
12631314 ) ;
12641315 }
12651316
0 commit comments