use serde::{Deserialize, Serialize};
use super::{
attestation_framework::{Attestation, INTENT_PREFIX_LENGTH},
handover::{HandoverAttestation, HandoverRecord},
headers::RecordKind,
snapshot::{SnapshotAttestation, SnapshotRecord},
RecordDigest, BLAKE3_HASH_SIZE, ED25519_SIGNATURE_SIZE,
};
use crate::consensus_crypto::{AuthorityPublicKey, ProtocolPublicKey};
const EPOCH_COMPLETE_INTENT_PREFIX: [u8; INTENT_PREFIX_LENGTH] =
RecordKind::EpochComplete.intent_prefix();
pub const EPOCH_COMPLETE_SIGNING_SIZE: usize =
INTENT_PREFIX_LENGTH + BLAKE3_HASH_SIZE + BLAKE3_HASH_SIZE;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct EpochCompleteMessage {
pub handover_attestation: HandoverAttestation,
pub snapshot_attestation: SnapshotAttestation,
#[serde(with = "serde_big_array::BigArray")]
pub unified_signature: [u8; ED25519_SIGNATURE_SIZE],
}
impl EpochCompleteMessage {
pub fn signing_message(
handover_digest: &RecordDigest,
snapshot_digest: &RecordDigest,
) -> [u8; EPOCH_COMPLETE_SIGNING_SIZE] {
let mut msg = [0u8; EPOCH_COMPLETE_SIGNING_SIZE];
msg[..INTENT_PREFIX_LENGTH].copy_from_slice(&EPOCH_COMPLETE_INTENT_PREFIX);
msg[INTENT_PREFIX_LENGTH..INTENT_PREFIX_LENGTH + BLAKE3_HASH_SIZE]
.copy_from_slice(handover_digest);
msg[INTENT_PREFIX_LENGTH + BLAKE3_HASH_SIZE..].copy_from_slice(snapshot_digest);
msg
}
pub fn authority_key(&self) -> &AuthorityPublicKey {
self.handover_attestation.authority_key()
}
pub fn protocol_key(&self) -> &ProtocolPublicKey {
self.handover_attestation.protocol_key()
}
pub fn sign(
handover_record: &HandoverRecord,
snapshot_record: &SnapshotRecord,
protocol_keypair: &crate::ProtocolKeyPair,
authority_key: &AuthorityPublicKey,
) -> Result<Self, &'static str> {
let handover_attestation =
Attestation::<HandoverRecord>::sign(handover_record, protocol_keypair, authority_key)
.map_err(|_| "EpochCompleteMessage: handover attestation signing failed")?;
let snapshot_attestation =
Attestation::<SnapshotRecord>::sign(snapshot_record, protocol_keypair, authority_key)
.map_err(|_| "EpochCompleteMessage: snapshot attestation signing failed")?;
if handover_record.dest_epoch() != snapshot_record.epoch() {
return Err("EpochCompleteMessage: handover dest_epoch does not match snapshot epoch");
}
if handover_attestation.authority_key() != snapshot_attestation.authority_key() {
return Err("EpochCompleteMessage: attestation authority keys are inconsistent");
}
if handover_attestation.protocol_key() != snapshot_attestation.protocol_key() {
return Err("EpochCompleteMessage: attestation protocol keys are inconsistent");
}
let handover_digest = handover_record.record_digest();
let snapshot_digest = snapshot_record.record_digest();
let msg = Self::signing_message(&handover_digest, &snapshot_digest);
let sig = protocol_keypair.sign(&msg);
let mut unified_signature = [0u8; ED25519_SIGNATURE_SIZE];
unified_signature.copy_from_slice(&sig.to_bytes());
if protocol_keypair.public().verify(&msg, &sig).is_err() {
return Err("EpochCompleteMessage unified signature self-verification failed");
}
Ok(Self {
handover_attestation,
snapshot_attestation,
unified_signature,
})
}
pub fn validate_internal_consistency(&self) -> Result<(), &'static str> {
if self.handover_attestation.authority_key() != self.snapshot_attestation.authority_key() {
return Err("EpochCompleteMessage: authority_key mismatch between handover and snapshot attestations");
}
if self.handover_attestation.protocol_key() != self.snapshot_attestation.protocol_key() {
return Err("EpochCompleteMessage: protocol_key mismatch between handover and snapshot attestations");
}
self.handover_attestation
.verify_internal_consistency()
.map_err(|_| {
"EpochCompleteMessage: handover attestation signature verification failed"
})?;
self.snapshot_attestation
.verify_internal_consistency()
.map_err(|_| {
"EpochCompleteMessage: snapshot attestation signature verification failed"
})?;
self.verify_unified_signature()
}
pub fn verify_unified_signature(&self) -> Result<(), &'static str> {
let handover_digest = self.handover_attestation.record_digest();
let snapshot_digest = self.snapshot_attestation.record_digest();
let msg = Self::signing_message(handover_digest, snapshot_digest);
let sig = crate::ProtocolKeySignature::from_bytes(&self.unified_signature)
.map_err(|_| "EpochCompleteMessage: invalid unified signature bytes")?;
self.protocol_key()
.verify(&msg, &sig)
.map_err(|_| "EpochCompleteMessage: unified signature verification failed")
}
}
impl std::fmt::Display for EpochCompleteMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"EpochCompleteMessage(authority={}ā¦, handover_digest={}ā¦, snapshot_digest={}ā¦)",
hex::encode(&self.authority_key().to_bytes()[..4]),
hex::encode(&self.handover_attestation.record_digest()[..4]),
hex::encode(&self.snapshot_attestation.record_digest()[..4]),
)
}
}
#[cfg(test)]
mod tests {
use super::{
super::test_helpers::{test_keys::*, test_records::*},
*,
};
fn create_test_epoch_complete_message(
seed: u64,
) -> (EpochCompleteMessage, crate::ProtocolKeyPair) {
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(seed);
let handover_record = create_test_handover_record(100, 0, 1, [0u8; 32]);
let snapshot_record = create_test_snapshot_record(100, 1);
let msg = EpochCompleteMessage::sign(
&handover_record,
&snapshot_record,
&protocol_keypair,
&authority_key,
)
.expect("EpochCompleteMessage::sign() should succeed");
(msg, protocol_keypair)
}
#[test]
fn test_epoch_complete_intent_prefix_matches_shared_crypto() {
use shared_crypto::intent::{AppId, IntentScope, IntentVersion};
assert_eq!(
EPOCH_COMPLETE_INTENT_PREFIX,
[
IntentScope::EpochComplete as u8,
IntentVersion::V0 as u8,
AppId::Consensus as u8,
],
"EPOCH_COMPLETE_INTENT_PREFIX is out of sync with shared-crypto enums"
);
}
#[test]
fn test_epoch_complete_signing_message_wire_compatible_with_bcs() {
use shared_crypto::intent::{Intent, IntentMessage, IntentScope};
let handover_digest = [0xAAu8; BLAKE3_HASH_SIZE];
let snapshot_digest = [0xBBu8; BLAKE3_HASH_SIZE];
let msg = EpochCompleteMessage::signing_message(&handover_digest, &snapshot_digest);
let intent_msg = IntentMessage::new(
Intent::consensus_app(IntentScope::EpochComplete),
handover_digest,
);
let bcs_bytes = bcs::to_bytes(&intent_msg).expect("BCS serialization should succeed");
assert_eq!(
msg[..INTENT_PREFIX_LENGTH],
bcs_bytes[..INTENT_PREFIX_LENGTH],
"intent prefix bytes must be byte-identical to bcs(IntentMessage(EpochComplete, _))"
);
}
#[test]
fn test_epoch_complete_signing_message_format_and_size() {
let handover_digest = [0xAAu8; BLAKE3_HASH_SIZE];
let snapshot_digest = [0xBBu8; BLAKE3_HASH_SIZE];
let msg = EpochCompleteMessage::signing_message(&handover_digest, &snapshot_digest);
assert_eq!(msg.len(), EPOCH_COMPLETE_SIGNING_SIZE);
assert_eq!(msg[..INTENT_PREFIX_LENGTH], EPOCH_COMPLETE_INTENT_PREFIX);
assert_eq!(
msg[INTENT_PREFIX_LENGTH..INTENT_PREFIX_LENGTH + BLAKE3_HASH_SIZE],
handover_digest
);
assert_eq!(
msg[INTENT_PREFIX_LENGTH + BLAKE3_HASH_SIZE..],
snapshot_digest
);
}
#[test]
fn test_epoch_complete_validate_internal_consistency_success() {
let (msg, _) = create_test_epoch_complete_message(5001);
assert!(msg.validate_internal_consistency().is_ok());
}
#[test]
fn test_epoch_complete_validate_internal_consistency_rejects_tampered_signature() {
let (mut msg, _) = create_test_epoch_complete_message(5002);
msg.unified_signature[0] ^= 0xFF;
assert!(msg.validate_internal_consistency().is_err());
}
#[test]
fn test_validate_internal_consistency_rejects_tampered_individual_attestation() {
let (mut msg, _) = create_test_epoch_complete_message(5050);
msg.handover_attestation.tamper_signature(0, 0xFF);
assert!(msg.validate_internal_consistency().is_err());
}
#[test]
fn test_epoch_complete_message_display() {
let (msg, _) = create_test_epoch_complete_message(5030);
let display = format!("{msg}");
assert!(display.contains("EpochCompleteMessage"));
assert!(display.contains("authority="));
assert!(display.contains("handover_digest="));
assert!(display.contains("snapshot_digest="));
}
#[test]
fn test_epoch_complete_sign_verify_roundtrip() {
let (msg, _) = create_test_epoch_complete_message(5040);
assert!(msg.verify_unified_signature().is_ok());
assert!(msg.validate_internal_consistency().is_ok());
msg.handover_attestation
.verify_internal_consistency()
.expect("handover attestation should verify");
msg.snapshot_attestation
.verify_internal_consistency()
.expect("snapshot attestation should verify");
}
#[test]
fn test_epoch_complete_verify_rejects_tampered_unified_signature() {
let (mut msg, _) = create_test_epoch_complete_message(5041);
msg.unified_signature[0] ^= 0xFF;
assert!(msg.verify_unified_signature().is_err());
}
#[test]
fn test_epoch_complete_sign_rejects_epoch_mismatch() {
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(5060);
let handover_record = create_test_handover_record(100, 0, 1, [0u8; 32]);
let snapshot_record = create_test_snapshot_record(100, 5);
let result = EpochCompleteMessage::sign(
&handover_record,
&snapshot_record,
&protocol_keypair,
&authority_key,
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("dest_epoch does not match snapshot epoch"));
}
}