Skip to content

Commit 645abc6

Browse files
committed
feat(stm): project SNARK AVK to a 44-bytes rigid slot
1 parent 4cce9a0 commit 645abc6

4 files changed

Lines changed: 234 additions & 78 deletions

File tree

mithril-common/src/entities/protocol_message.rs

Lines changed: 125 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ use thiserror::Error;
88
#[cfg(test)]
99
use 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)]
1518
pub 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")]
6072
fn 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")]
7890
fn 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

mithril-common/src/signable_builder/signable_builder_service.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,7 @@ mod tests {
597597
assert!(
598598
matches!(
599599
integrity_error,
600-
crate::entities::RigidProtocolMessageIntegrityError::UnexpectedFieldLength {
601-
field: "next_aggregate_verification_key",
602-
..
603-
}
600+
crate::entities::RigidProtocolMessageIntegrityError::InvalidSnarkAggregateVerificationKey(_)
604601
),
605602
"unexpected error variant: {integrity_error:?}"
606603
);

0 commit comments

Comments
 (0)