// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Admin transaction types for privileged network operations.
//!
//! This module contains types for administrative transactions that can
//! only be submitted by authorized parties (e.g., epoch changes).

use rialo_s_pubkey::Pubkey;
use serde::{Deserialize, Serialize};

use crate::{
    consensus_crypto::{AuthorityPublicKey, NetworkPublicKey, ProtocolPublicKey},
    Multiaddr,
};

/// Size of a Blake3 hash in bytes.
pub const BLAKE3_HASH_SIZE: usize = 32;

/// Blake3 hash type alias (32 bytes).
pub type Blake3Hash = [u8; BLAKE3_HASH_SIZE];

/// An identifier for an epoch.
///
/// This is a newtype wrapper around `u64` to provide type safety when working
/// with epoch numbers, preventing accidental mixing with other numeric types.
#[derive(
    Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
)]
#[repr(transparent)]
pub struct EpochIdentifier(u64);

impl EpochIdentifier {
    /// Creates a new epoch identifier from a `u64`.
    pub const fn new(epoch: u64) -> Self {
        Self(epoch)
    }

    /// Returns the epoch as a `u64`.
    pub const fn as_u64(self) -> u64 {
        self.0
    }

    /// Returns the bytes of the epoch in little-endian order.
    pub const fn to_le_bytes(self) -> [u8; 8] {
        self.0.to_le_bytes()
    }
}

impl From<u64> for EpochIdentifier {
    fn from(epoch: u64) -> Self {
        Self(epoch)
    }
}

impl From<EpochIdentifier> for u64 {
    fn from(epoch: EpochIdentifier) -> Self {
        epoch.0
    }
}

impl std::fmt::Display for EpochIdentifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// BLS12-381 public key size in bytes.
pub const BLS12381_PUBLIC_KEY_SIZE: usize = 96;

/// Ed25519 public key size in bytes.
pub const ED25519_PUBLIC_KEY_SIZE: usize = 32;
/// Ed25519 signature size in bytes.
pub const ED25519_SIGNATURE_SIZE: usize = 64;

/// Configuration for a new epoch's validator set and consensus parameters.
///
/// This is submitted via the `submitEpochChange` RPC endpoint to initiate
/// an epoch transition. The configuration specifies the complete set of
/// validators for the new epoch along with their networking information.
///
/// Note: If a validator is staying across epochs, it must run two consensus
/// protocols simultaneously, so the network ports for the new epoch must be
/// different from the old epoch's ports.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct EpochChangeConfig {
    /// The current epoch number (the epoch being transitioned from).
    /// This must match the actual current epoch when the transaction is processed.
    /// TODO: Instead of using a number for the current epoch we should use the hash of the certificate for the epoch - <https://linear.app/subzero-labs/issue/SUB-1505/create-a-chain-of-governance-tx>
    pub current_epoch: EpochIdentifier,

    /// The epoch number being transitioned to.
    /// This must be greater than `current_epoch`.
    pub new_epoch: EpochIdentifier,

    /// The validators participating in the new epoch.
    /// Each validator entry contains their stake, network addresses, and keys.
    pub validators: Vec<ValidatorInfo>,

    /// Optional consensus configuration overrides for the new epoch.
    /// If not provided, the current epoch's configuration will be used.
    pub consensus_config: Option<EpochConsensusConfig>,
}

impl EpochChangeConfig {
    /// Computes a deterministic Blake3 hash of this epoch change configuration.
    ///
    /// The hash is computed by serializing the configuration in a canonical format:
    /// - current_epoch (8 bytes LE)
    /// - new_epoch (8 bytes LE)
    /// - number of validators (8 bytes LE)
    /// - for each validator (in order):
    ///   - stake (8 bytes LE)
    ///   - consensus_address (length-prefixed bytes)
    ///   - state_sync_address (length-prefixed bytes)
    ///   - hostname (length-prefixed bytes)
    ///   - authority_key (96 bytes)
    ///   - protocol_key (32 bytes)
    ///   - network_key (32 bytes)
    ///   - signing_key (32 bytes)
    /// - consensus_config presence flag (1 byte: 0 or 1)
    /// - if consensus_config is present:
    ///   - each optional field with presence flag and value
    ///
    /// This produces a stable hash that uniquely identifies the configuration.
    ///
    /// # Deployment note
    ///
    /// Any change to this function's encoding is **hash-breaking** and requires a
    /// **stop-the-world deployment** — all validators must upgrade simultaneously.
    pub fn deterministic_hash(&self) -> [u8; BLAKE3_HASH_SIZE] {
        let mut hasher = blake3::Hasher::new();

        // Hash epoch numbers
        hasher.update(&self.current_epoch.to_le_bytes());
        hasher.update(&self.new_epoch.to_le_bytes());

        // Hash validators count and each validator
        hasher.update(&(self.validators.len() as u64).to_le_bytes());
        for validator in &self.validators {
            hasher.update(&validator.stake.to_le_bytes());

            // Hash addresses as length-prefixed canonical wire-format bytes.
            // IMPORTANT: Uses Multiaddr::to_vec() (canonical binary encoding) rather than
            // to_string() (display representation) to ensure deterministic hashing across
            // library versions and platforms. Note that serde Serialize/Deserialize for
            // Multiaddr intentionally uses the human-readable string form (for JSON/RPC
            // ergonomics) — only this hash function requires the wire format.
            let consensus_addr_bytes = validator.consensus_address.to_vec();
            hasher.update(&(consensus_addr_bytes.len() as u64).to_le_bytes());
            hasher.update(&consensus_addr_bytes);

            let state_sync_addr_bytes = validator.state_sync_address.to_vec();
            hasher.update(&(state_sync_addr_bytes.len() as u64).to_le_bytes());
            hasher.update(&state_sync_addr_bytes);

            // Hash hostname
            hasher.update(&(validator.hostname.len() as u64).to_le_bytes());
            hasher.update(validator.hostname.as_bytes());

            // Hash keys
            hasher.update(&validator.authority_key.to_bytes());
            hasher.update(&validator.protocol_key.to_bytes());
            hasher.update(&validator.network_key.to_bytes());
            hasher.update(validator.signing_key.as_ref());
        }

        // Hash consensus config
        match &self.consensus_config {
            Some(config) => {
                hasher.update(&[1u8]); // Present flag

                // Hash each optional field with presence flag
                Self::hash_optional_usize(&mut hasher, config.num_leaders_per_round);
                Self::hash_optional_u64(&mut hasher, config.max_transactions_in_block_bytes);
                Self::hash_optional_u64(&mut hasher, config.max_num_transactions_in_block);
                Self::hash_optional_u64(&mut hasher, config.max_transaction_size_bytes);
                Self::hash_optional_u32(&mut hasher, config.gc_depth);
                Self::hash_optional_bool(&mut hasher, config.zstd_compression);
            }
            None => {
                hasher.update(&[0u8]); // Absent flag
            }
        }

        *hasher.finalize().as_bytes()
    }

    fn hash_optional_usize(hasher: &mut blake3::Hasher, value: Option<usize>) {
        match value {
            Some(v) => {
                hasher.update(&[1u8]);
                hasher.update(&(v as u64).to_le_bytes());
            }
            None => {
                hasher.update(&[0u8]);
            }
        }
    }

    fn hash_optional_u64(hasher: &mut blake3::Hasher, value: Option<u64>) {
        match value {
            Some(v) => {
                hasher.update(&[1u8]);
                hasher.update(&v.to_le_bytes());
            }
            None => {
                hasher.update(&[0u8]);
            }
        }
    }

    fn hash_optional_u32(hasher: &mut blake3::Hasher, value: Option<u32>) {
        match value {
            Some(v) => {
                hasher.update(&[1u8]);
                hasher.update(&v.to_le_bytes());
            }
            None => {
                hasher.update(&[0u8]);
            }
        }
    }

    fn hash_optional_bool(hasher: &mut blake3::Hasher, value: Option<bool>) {
        match value {
            Some(v) => {
                hasher.update(&[1u8]);
                hasher.update(&[v as u8]);
            }
            None => {
                hasher.update(&[0u8]);
            }
        }
    }
}

/// Information about a validator in the new epoch.
///
/// Contains all the necessary information to identify and communicate
/// with a validator in the consensus protocol.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ValidatorInfo {
    /// Voting power/stake of the validator.
    pub stake: u64,

    /// Network address for consensus protocol communication.
    /// Format: multiaddr (e.g., "/ip4/127.0.0.1/udp/10100")
    pub consensus_address: Multiaddr,

    /// Network address for state sync communication.
    /// Format: multiaddr (e.g., "/ip4/127.0.0.1/udp/20100")
    pub state_sync_address: Multiaddr,

    /// Human-readable hostname for metrics and logging.
    pub hostname: String,

    /// The validator's BLS12-381 public key for authority identity.
    pub authority_key: AuthorityPublicKey,

    /// The validator's Ed25519 public key for verifying blocks.
    pub protocol_key: ProtocolPublicKey,

    /// The validator's Ed25519 public key for TLS and network identity.
    pub network_key: NetworkPublicKey,

    /// The validator's public key for signing transactions.
    pub signing_key: Pubkey,
}

/// Consensus configuration parameters that can be adjusted per-epoch.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct EpochConsensusConfig {
    /// Number of leaders per round for Mysticeti commits.
    pub num_leaders_per_round: Option<usize>,

    /// Maximum size of transactions included in a consensus block (bytes).
    pub max_transactions_in_block_bytes: Option<u64>,

    /// Maximum number of transactions included in a consensus block.
    pub max_num_transactions_in_block: Option<u64>,

    /// Maximum serialized transaction size (bytes).
    pub max_transaction_size_bytes: Option<u64>,

    /// Garbage collection depth for consensus.
    pub gc_depth: Option<u32>,

    /// Enable zstd compression for consensus network.
    pub zstd_compression: Option<bool>,
}

/// Intent prefix for ValidatorReadyMessage signatures.
/// `[scope=ValidatorReady=14, version=V0=0, app_id=Consensus=2]`
///
/// These values mirror `shared_crypto::intent::{IntentScope, IntentVersion, AppId}`
/// but are inlined to avoid pulling `shared-crypto` (and its `fastcrypto` transitive
/// dependency) into RISC-V / PDK builds where it cannot compile. A `#[cfg(test)]`
/// assertion in the test module verifies these stay in sync.
///
/// This domain separator prevents cross-domain signature replay — a signature
/// over a message in a different domain cannot be mistaken for a valid ready message.
const READY_MESSAGE_INTENT_PREFIX: [u8; 3] = [14, 0, 2];

/// Length of the intent prefix (must equal `shared_crypto::intent::INTENT_PREFIX_LENGTH`).
const INTENT_PREFIX_LEN: usize = 3;

/// Size of the signing message for ValidatorReadyMessage.
/// Format: intent_prefix (3 bytes) || current_epoch (8 bytes) || epoch_config_hash (32 bytes) = 43 bytes
///
/// STOP-THE-WORLD: Changing this constant (or the intent prefix bytes) is a
/// signature-breaking change. All validators must upgrade simultaneously.
pub const READY_MESSAGE_SIGNING_SIZE: usize = INTENT_PREFIX_LEN + 8 + BLAKE3_HASH_SIZE;

/// A ready message sent by a validator to signal readiness for an epoch change.
///
/// This message is broadcast by validators in either the current epoch or the
/// proposed new epoch when they see an `AdminTransaction::EpochChange`. The message
/// includes the validator's signature over the epoch transition.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ValidatorReadyMessage {
    /// The current epoch number (the epoch being transitioned from).
    /// TODO: Instead of using a number for the current epoch we should use the hash of the certificate for the epoch - <https://linear.app/subzero-labs/issue/SUB-1505/create-a-chain-of-governance-tx>
    pub current_epoch: EpochIdentifier,

    /// The deterministic hash of the EpochChangeConfig being agreed upon.
    /// This binds the ready message to a specific epoch configuration, preventing
    /// validators from accidentally agreeing to different configurations.
    pub epoch_config_hash: [u8; BLAKE3_HASH_SIZE],

    /// The validator's authority public key identifying the sender.
    /// This is used to verify the sender is a validator in the current or new epoch.
    pub authority_key: AuthorityPublicKey,

    /// The validator's protocol public key used to verify the signature.
    pub protocol_key: ProtocolPublicKey,

    /// Ed25519 signature over the intent-prefixed epoch transition (43 bytes).
    /// The signature is created using the validator's protocol key.
    #[serde(with = "serde_big_array::BigArray")]
    pub signature: [u8; ED25519_SIGNATURE_SIZE],
}

impl ValidatorReadyMessage {
    /// Returns the message bytes that should be signed.
    ///
    /// The message format is: intent_prefix (3 bytes) || current_epoch (8 bytes LE) || epoch_config_hash (32 bytes)
    ///
    /// The intent prefix is a domain separator from the shared-crypto intent system
    /// (scope=ValidatorReady, version=V0, app_id=Consensus) that prevents cross-domain
    /// signature replay.
    pub fn signing_message(
        current_epoch: EpochIdentifier,
        epoch_config_hash: &[u8; BLAKE3_HASH_SIZE],
    ) -> [u8; READY_MESSAGE_SIGNING_SIZE] {
        let mut msg = [0u8; READY_MESSAGE_SIGNING_SIZE];
        msg[0..3].copy_from_slice(&READY_MESSAGE_INTENT_PREFIX);
        msg[3..11].copy_from_slice(&current_epoch.to_le_bytes());
        msg[11..READY_MESSAGE_SIGNING_SIZE].copy_from_slice(epoch_config_hash);
        msg
    }

    /// Returns the message bytes for this ready message.
    pub fn message_bytes(&self) -> [u8; READY_MESSAGE_SIGNING_SIZE] {
        Self::signing_message(self.current_epoch, &self.epoch_config_hash)
    }
}

/// A handover certificate marking an epoch transition.
///
/// This is auto-generated by BlockPool when Ready quorum is reached.
/// Contains the full EpochChangeConfig, making the certificate self-contained
/// for syncing nodes to verify and process epoch transitions.
///
/// Note: This is the unsigned certificate. `SignedHandoverCertificate` (with
/// validator signatures) will be added in a future phase.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct HandoverCertificate {
    /// Block index of this Handover in the main chain.
    pub block_index: u64,

    /// The epoch being transitioned from.
    pub source_epoch: EpochIdentifier,

    /// The epoch being transitioned to.
    pub dest_epoch: EpochIdentifier,

    /// Full configuration for the new epoch (self-contained).
    /// This allows syncing nodes to verify the epoch transition without
    /// needing to look up the original EpochChange transaction.
    pub epoch_config: EpochChangeConfig,

    /// Blake3 hash of the previous HandoverCertificate (forms skiplist chain).
    /// - Genesis: zero hash `[0u8; 32]`
    /// - Subsequent: Blake3 hash of the previous certificate
    pub previous_handover_hash: Blake3Hash,
}

impl HandoverCertificate {
    /// Computes the deterministic Blake3 hash of this certificate.
    ///
    /// This hash is used as `previous_handover_hash` in the next certificate,
    /// forming the skiplist chain.
    pub fn deterministic_hash(&self) -> Blake3Hash {
        let mut hasher = blake3::Hasher::new();

        // Hash block index
        hasher.update(&self.block_index.to_le_bytes());

        // Hash epoch identifiers
        hasher.update(&self.source_epoch.to_le_bytes());
        hasher.update(&self.dest_epoch.to_le_bytes());

        // Hash the epoch config using its deterministic hash
        hasher.update(&self.epoch_config.deterministic_hash());

        // Hash the previous handover hash
        hasher.update(&self.previous_handover_hash);

        *hasher.finalize().as_bytes()
    }

    /// Creates a genesis handover certificate.
    ///
    /// Genesis is a degenerate certificate where `source_epoch == dest_epoch`
    /// and `previous_handover_hash` is zero.
    pub fn genesis(epoch: EpochIdentifier, epoch_config: EpochChangeConfig) -> Self {
        Self {
            block_index: 0,
            source_epoch: epoch,
            dest_epoch: epoch,
            epoch_config,
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        }
    }

    /// Returns true if this is a genesis certificate.
    ///
    /// A genesis certificate has `source_epoch == dest_epoch == 0`.
    pub fn is_genesis(&self) -> bool {
        self.source_epoch == EpochIdentifier::from(0) && self.dest_epoch == EpochIdentifier::from(0)
    }
}

/// A snapshot attestation from a validator.
///
/// This message is generated by validators after they successfully create a snapshot,
/// attesting to the existence of a snapshot at a specific block height with a given
/// key-value hash. When f+1 validators attest to the same snapshot, it becomes
/// a certified snapshot.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SnapshotAttestation {
    /// The block height at which the snapshot was taken.
    pub block_height: u64,

    /// The Blake3 hash of the snapshot's key-value state.
    pub keysvals_hash: [u8; BLAKE3_HASH_SIZE],

    /// The validator's authority public key identifying the sender.
    pub authority_key: AuthorityPublicKey,

    /// The validator's protocol public key used to verify the signature.
    pub protocol_key: ProtocolPublicKey,

    /// Ed25519 signature over (block_height || keysvals_hash) as 40 bytes.
    /// The signature is created using the validator's protocol key.
    #[serde(with = "serde_big_array::BigArray")]
    pub signature: [u8; ED25519_SIGNATURE_SIZE],
}

/// Size of the signing message for SnapshotAttestation.
/// Format: block_height (8 bytes) || keysvals_hash (32 bytes) = 40 bytes
pub const SNAPSHOT_ATTESTATION_SIGNING_SIZE: usize = 8 + BLAKE3_HASH_SIZE;

impl SnapshotAttestation {
    /// Returns the message bytes that should be signed for a snapshot attestation.
    ///
    /// The message format is: block_height (8 bytes LE) || keysvals_hash (32 bytes)
    pub fn signing_message(
        block_height: u64,
        keysvals_hash: &[u8; BLAKE3_HASH_SIZE],
    ) -> [u8; SNAPSHOT_ATTESTATION_SIGNING_SIZE] {
        let mut msg = [0u8; SNAPSHOT_ATTESTATION_SIGNING_SIZE];
        msg[0..8].copy_from_slice(&block_height.to_le_bytes());
        msg[8..SNAPSHOT_ATTESTATION_SIGNING_SIZE].copy_from_slice(keysvals_hash);
        msg
    }

    /// Returns the message bytes for this attestation.
    pub fn message_bytes(&self) -> [u8; SNAPSHOT_ATTESTATION_SIGNING_SIZE] {
        Self::signing_message(self.block_height, &self.keysvals_hash)
    }
}

/// Wrapper for admin transactions that go through consensus.
///
/// Admin transactions are privileged operations that are processed
/// separately from regular user transactions and REX updates.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AdminTransaction {
    /// Initiates an epoch change with the specified configuration.
    EpochChange(Box<EpochChangeConfig>),

    /// A ready message from a validator signaling readiness for an epoch change.
    ///
    /// This is sent by validators in the current or new epoch after they see
    /// an `EpochChange` transaction. Validators must collect a quorum of these
    /// ready messages before the epoch transition can complete.
    Ready(Box<ValidatorReadyMessage>),

    /// A handover certificate marking an epoch transition.
    ///
    /// This is auto-generated by BlockPool when Ready quorum is reached.
    /// The Node detects epoch transitions by parsing this transaction from
    /// committed blocks.
    Handover(Box<HandoverCertificate>),

    /// A snapshot attestation from a validator.
    ///
    /// This is sent by validators after they create a snapshot, attesting to the
    /// snapshot's existence and hash. When f+1 validators attest to the same
    /// snapshot, it becomes certified and can be used for recovery.
    SnapshotAttestation(Box<SnapshotAttestation>),
}

#[cfg(test)]
mod tests {
    use fastcrypto::{ed25519, traits::KeyPair};
    use rand::SeedableRng;

    use super::*;
    use crate::consensus_crypto::{AuthorityPublicKey, NetworkPublicKey, ProtocolPublicKey};

    /// Helper to create test keys with a given seed for deterministic tests.
    fn create_test_keys(seed: u64) -> (AuthorityPublicKey, ProtocolPublicKey, NetworkPublicKey) {
        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
        let authority_key = AuthorityPublicKey::new(
            fastcrypto::bls12381::min_sig::BLS12381KeyPair::generate(&mut rng)
                .public()
                .clone(),
        );
        let protocol_key =
            ProtocolPublicKey::new(ed25519::Ed25519KeyPair::generate(&mut rng).public().clone());
        let network_key =
            NetworkPublicKey::new(ed25519::Ed25519KeyPair::generate(&mut rng).public().clone());
        (authority_key, protocol_key, network_key)
    }

    #[test]
    fn test_epoch_change_config_serialization() {
        let (authority_key, protocol_key, network_key) = create_test_keys(42);
        let config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![ValidatorInfo {
                stake: 1000,
                consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
                state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
                hostname: "validator-0".to_string(),
                authority_key,
                protocol_key,
                network_key,
                signing_key: Pubkey::new_unique(),
            }],
            consensus_config: None,
        };

        let serialized = serde_json::to_string(&config).expect("serialization should succeed");
        let deserialized: EpochChangeConfig =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        assert_eq!(deserialized.current_epoch, config.current_epoch);
        assert_eq!(deserialized.new_epoch, config.new_epoch);
        assert_eq!(deserialized.validators.len(), 1);
        assert_eq!(deserialized.validators[0].stake, 1000);
    }

    #[test]
    fn test_admin_transaction_serialization() {
        let config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(4),
            new_epoch: EpochIdentifier::new(5),
            validators: vec![],
            consensus_config: Some(EpochConsensusConfig {
                num_leaders_per_round: Some(2),
                ..Default::default()
            }),
        };

        let admin_tx = AdminTransaction::EpochChange(Box::new(config));
        let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
        let deserialized: AdminTransaction =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        match deserialized {
            AdminTransaction::EpochChange(cfg) => {
                assert_eq!(cfg.new_epoch, EpochIdentifier::new(5));
                assert!(cfg.consensus_config.is_some());
            }
            _ => panic!("Expected EpochChange"),
        }
    }

    #[test]
    fn test_validator_ready_message_serialization() {
        let (authority_key, protocol_key, _) = create_test_keys(43);
        let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
        let ready_msg = ValidatorReadyMessage {
            current_epoch: EpochIdentifier::new(0),
            epoch_config_hash,
            authority_key: authority_key.clone(),
            protocol_key: protocol_key.clone(),
            signature: [2u8; ED25519_SIGNATURE_SIZE],
        };

        let serialized = serde_json::to_string(&ready_msg).expect("serialization should succeed");
        let deserialized: ValidatorReadyMessage =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        assert_eq!(deserialized.current_epoch, EpochIdentifier::new(0));
        assert_eq!(deserialized.epoch_config_hash, epoch_config_hash);
        assert_eq!(deserialized.authority_key, authority_key);
        assert_eq!(deserialized.protocol_key, protocol_key);
        assert_eq!(deserialized.signature, ready_msg.signature);
    }

    /// Verifies `message_bytes()` on a struct delegates to `signing_message()`.
    #[test]
    fn test_validator_ready_message_bytes_delegates_to_signing_message() {
        let (authority_key, protocol_key, _) = create_test_keys(44);
        let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
        let ready_msg = ValidatorReadyMessage {
            current_epoch: EpochIdentifier::new(5),
            epoch_config_hash,
            authority_key,
            protocol_key,
            signature: [2u8; ED25519_SIGNATURE_SIZE],
        };

        assert_eq!(
            ready_msg.message_bytes(),
            ValidatorReadyMessage::signing_message(EpochIdentifier::new(5), &epoch_config_hash)
        );
    }

    /// Verifies the exact wire format of the signing message with the intent prefix.
    /// The signing message is: intent_prefix (3 bytes) || current_epoch (8 bytes LE) || epoch_config_hash (32 bytes) = 43 bytes.
    #[test]
    fn test_ready_message_signing_message_wire_format() {
        let epoch = EpochIdentifier::new(42);
        let epoch_config_hash = [0xBBu8; BLAKE3_HASH_SIZE];

        let msg = ValidatorReadyMessage::signing_message(epoch, &epoch_config_hash);

        // Total size: 3 (intent) + 8 (epoch) + 32 (hash) = 43
        assert_eq!(msg.len(), 43);

        // First 3 bytes: intent prefix [scope=ValidatorReady=14, version=V0=0, app_id=Consensus=2]
        assert_eq!(msg[0..3], [14, 0, 2]);

        // Bytes 3..11: current_epoch as little-endian u64
        assert_eq!(msg[3..11], 42u64.to_le_bytes());

        // Bytes 11..43: epoch_config_hash
        assert_eq!(msg[11..43], epoch_config_hash);

        // Different inputs produce different messages
        let msg2 =
            ValidatorReadyMessage::signing_message(EpochIdentifier::new(0), &epoch_config_hash);
        assert_ne!(msg, msg2);
        // But the intent prefix is always the same
        assert_eq!(msg2[0..3], [14, 0, 2]);
    }

    /// Guards against the inlined intent constants drifting from `shared-crypto`.
    #[test]
    fn test_ready_message_intent_prefix_matches_shared_crypto() {
        use shared_crypto::intent::{AppId, IntentScope, IntentVersion, INTENT_PREFIX_LENGTH};

        assert_eq!(
            super::READY_MESSAGE_INTENT_PREFIX,
            [
                IntentScope::ValidatorReady as u8,
                IntentVersion::V0 as u8,
                AppId::Consensus as u8,
            ],
            "READY_MESSAGE_INTENT_PREFIX is out of sync with shared-crypto enums"
        );
        assert_eq!(
            super::INTENT_PREFIX_LEN,
            INTENT_PREFIX_LENGTH,
            "INTENT_PREFIX_LEN is out of sync with shared_crypto::intent::INTENT_PREFIX_LENGTH"
        );
    }

    #[test]
    fn test_admin_transaction_ready_serialization() {
        let (authority_key, protocol_key, _) = create_test_keys(45);
        let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
        let ready_msg = ValidatorReadyMessage {
            current_epoch: EpochIdentifier::new(0),
            epoch_config_hash,
            authority_key,
            protocol_key,
            signature: [2u8; ED25519_SIGNATURE_SIZE],
        };

        let admin_tx = AdminTransaction::Ready(Box::new(ready_msg));
        let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
        let deserialized: AdminTransaction =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        match deserialized {
            AdminTransaction::Ready(msg) => {
                assert_eq!(msg.current_epoch, EpochIdentifier::new(0));
                assert_eq!(msg.epoch_config_hash, epoch_config_hash);
            }
            _ => panic!("Expected Ready"),
        }
    }

    #[test]
    fn test_handover_certificate_serialization() {
        let epoch_config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![],
            consensus_config: None,
        };

        let cert = HandoverCertificate {
            block_index: 42,
            source_epoch: EpochIdentifier::new(0),
            dest_epoch: EpochIdentifier::new(1),
            epoch_config,
            previous_handover_hash: [0xABu8; BLAKE3_HASH_SIZE],
        };

        let serialized = serde_json::to_string(&cert).expect("serialization should succeed");
        let deserialized: HandoverCertificate =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        assert_eq!(deserialized.block_index, 42);
        assert_eq!(deserialized.source_epoch, EpochIdentifier::new(0));
        assert_eq!(deserialized.dest_epoch, EpochIdentifier::new(1));
        assert_eq!(
            deserialized.previous_handover_hash,
            [0xABu8; BLAKE3_HASH_SIZE]
        );
    }

    #[test]
    fn test_handover_certificate_deterministic_hash() {
        let epoch_config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![],
            consensus_config: None,
        };

        let cert1 = HandoverCertificate {
            block_index: 42,
            source_epoch: EpochIdentifier::new(0),
            dest_epoch: EpochIdentifier::new(1),
            epoch_config: epoch_config.clone(),
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        };

        // Same cert should produce same hash
        let cert2 = cert1.clone();
        assert_eq!(cert1.deterministic_hash(), cert2.deterministic_hash());

        // Different block_index should produce different hash
        let cert3 = HandoverCertificate {
            block_index: 43,
            ..cert1.clone()
        };
        assert_ne!(cert1.deterministic_hash(), cert3.deterministic_hash());

        // Different previous_handover_hash should produce different hash
        let cert4 = HandoverCertificate {
            previous_handover_hash: [1u8; BLAKE3_HASH_SIZE],
            ..cert1.clone()
        };
        assert_ne!(cert1.deterministic_hash(), cert4.deterministic_hash());
    }

    #[test]
    fn test_handover_certificate_genesis() {
        let epoch_config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(0),
            validators: vec![],
            consensus_config: None,
        };

        let genesis = HandoverCertificate::genesis(EpochIdentifier::new(0), epoch_config.clone());

        assert_eq!(genesis.block_index, 0);
        assert_eq!(genesis.source_epoch, EpochIdentifier::new(0));
        assert_eq!(genesis.dest_epoch, EpochIdentifier::new(0));
        assert_eq!(genesis.previous_handover_hash, [0u8; BLAKE3_HASH_SIZE]);
        assert_eq!(genesis.epoch_config, epoch_config);
    }

    #[test]
    fn test_handover_certificate_is_genesis() {
        let epoch_config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(0),
            validators: vec![],
            consensus_config: None,
        };

        // Genesis certificate: source_epoch == dest_epoch == 0
        let genesis = HandoverCertificate::genesis(EpochIdentifier::new(0), epoch_config.clone());
        assert!(genesis.is_genesis());

        // Non-genesis: source_epoch = 0, dest_epoch = 1
        let handover_0_to_1 = HandoverCertificate {
            block_index: 100,
            source_epoch: EpochIdentifier::new(0),
            dest_epoch: EpochIdentifier::new(1),
            epoch_config: epoch_config.clone(),
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        };
        assert!(!handover_0_to_1.is_genesis());

        // Non-genesis: source_epoch = 1, dest_epoch = 1 (degenerate but not epoch 0)
        let handover_1_to_1 = HandoverCertificate {
            block_index: 200,
            source_epoch: EpochIdentifier::new(1),
            dest_epoch: EpochIdentifier::new(1),
            epoch_config: epoch_config.clone(),
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        };
        assert!(!handover_1_to_1.is_genesis());

        // Non-genesis: source_epoch = 1, dest_epoch = 2
        let handover_1_to_2 = HandoverCertificate {
            block_index: 300,
            source_epoch: EpochIdentifier::new(1),
            dest_epoch: EpochIdentifier::new(2),
            epoch_config,
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        };
        assert!(!handover_1_to_2.is_genesis());
    }

    #[test]
    fn test_admin_transaction_handover_serialization() {
        let epoch_config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![],
            consensus_config: None,
        };

        let cert = HandoverCertificate {
            block_index: 100,
            source_epoch: EpochIdentifier::new(0),
            dest_epoch: EpochIdentifier::new(1),
            epoch_config,
            previous_handover_hash: [0u8; BLAKE3_HASH_SIZE],
        };

        let admin_tx = AdminTransaction::Handover(Box::new(cert));
        let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
        let deserialized: AdminTransaction =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        match deserialized {
            AdminTransaction::Handover(c) => {
                assert_eq!(c.block_index, 100);
                assert_eq!(c.source_epoch, EpochIdentifier::new(0));
                assert_eq!(c.dest_epoch, EpochIdentifier::new(1));
            }
            _ => panic!("Expected Handover"),
        }
    }

    #[test]
    fn test_epoch_change_config_deterministic_hash() {
        let (authority_key, protocol_key, network_key) = create_test_keys(50);
        let config1 = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![ValidatorInfo {
                stake: 1000,
                consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
                state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
                hostname: "validator-0".to_string(),
                authority_key: authority_key.clone(),
                protocol_key: protocol_key.clone(),
                network_key: network_key.clone(),
                signing_key: Pubkey::new_from_array([3u8; 32]),
            }],
            consensus_config: None,
        };

        // Same config should produce same hash
        let config2 = config1.clone();
        assert_eq!(config1.deterministic_hash(), config2.deterministic_hash());

        // Different new_epoch should produce different hash
        let config3 = EpochChangeConfig {
            new_epoch: EpochIdentifier::new(2),
            ..config1.clone()
        };
        assert_ne!(config1.deterministic_hash(), config3.deterministic_hash());

        // Different validators should produce different hash
        let config4 = EpochChangeConfig {
            validators: vec![ValidatorInfo {
                stake: 2000, // Different stake
                consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
                state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
                hostname: "validator-0".to_string(),
                authority_key,
                protocol_key,
                network_key,
                signing_key: Pubkey::new_from_array([3u8; 32]),
            }],
            ..config1.clone()
        };
        assert_ne!(config1.deterministic_hash(), config4.deterministic_hash());

        // Adding consensus config should produce different hash
        let config5 = EpochChangeConfig {
            consensus_config: Some(EpochConsensusConfig {
                num_leaders_per_round: Some(2),
                ..Default::default()
            }),
            ..config1.clone()
        };
        assert_ne!(config1.deterministic_hash(), config5.deterministic_hash());
    }

    /// Regression test: `deterministic_hash()` must use canonical wire-format bytes
    /// (`Multiaddr::to_vec()`) for address fields, not the display string (`to_string()`).
    /// This test manually recomputes the expected hash using wire-format bytes and pins it,
    /// so any accidental revert to string-based hashing will be caught.
    #[test]
    fn test_deterministic_hash_uses_canonical_multiaddr_wire_format() {
        let (authority_key, protocol_key, network_key) = create_test_keys(50);
        let config = EpochChangeConfig {
            current_epoch: EpochIdentifier::new(0),
            new_epoch: EpochIdentifier::new(1),
            validators: vec![ValidatorInfo {
                stake: 1000,
                consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
                state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
                hostname: "validator-0".to_string(),
                authority_key: authority_key.clone(),
                protocol_key: protocol_key.clone(),
                network_key: network_key.clone(),
                signing_key: Pubkey::new_from_array([3u8; 32]),
            }],
            consensus_config: None,
        };

        // Manually compute the expected hash using canonical wire-format bytes for addresses.
        let mut hasher = blake3::Hasher::new();
        hasher.update(&0u64.to_le_bytes()); // current_epoch
        hasher.update(&1u64.to_le_bytes()); // new_epoch
        hasher.update(&1u64.to_le_bytes()); // validators.len()

        hasher.update(&1000u64.to_le_bytes()); // stake

        // Addresses: canonical wire-format bytes via to_vec()
        let consensus_addr: crate::multiaddr::Multiaddr =
            "/ip4/127.0.0.1/udp/10100".parse().unwrap();
        let consensus_wire = consensus_addr.to_vec();
        hasher.update(&(consensus_wire.len() as u64).to_le_bytes());
        hasher.update(&consensus_wire);

        let state_sync_addr: crate::multiaddr::Multiaddr =
            "/ip4/127.0.0.1/udp/20100".parse().unwrap();
        let state_sync_wire = state_sync_addr.to_vec();
        hasher.update(&(state_sync_wire.len() as u64).to_le_bytes());
        hasher.update(&state_sync_wire);

        // hostname
        let hostname = "validator-0";
        hasher.update(&(hostname.len() as u64).to_le_bytes());
        hasher.update(hostname.as_bytes());

        // keys
        hasher.update(&authority_key.to_bytes());
        hasher.update(&protocol_key.to_bytes());
        hasher.update(&network_key.to_bytes());
        hasher.update(Pubkey::new_from_array([3u8; 32]).as_ref());

        // consensus_config absent
        hasher.update(&[0u8]);

        let expected_hash = *hasher.finalize().as_bytes();
        assert_eq!(
            config.deterministic_hash(),
            expected_hash,
            "deterministic_hash() must use Multiaddr::to_vec() canonical wire-format bytes, \
             not to_string() display representation"
        );

        // Hash must be stable across invocations
        assert_eq!(config.deterministic_hash(), config.deterministic_hash());
    }

    /// Demonstrates that Multiaddr wire-format bytes differ from the display string bytes.
    /// This is the root cause that the canonical encoding fix addresses.
    #[test]
    fn test_multiaddr_to_string_differs_from_to_vec() {
        let addr: crate::multiaddr::Multiaddr = "/ip4/127.0.0.1/udp/10100".parse().unwrap();

        let string_bytes = addr.to_string().into_bytes();
        let wire_bytes = addr.to_vec();

        assert_ne!(
            string_bytes, wire_bytes,
            "to_string() bytes and to_vec() wire-format bytes must differ — \
             using the wrong one for hashing would silently break consensus"
        );
    }

    #[test]
    fn test_snapshot_attestation_serialization() {
        let (authority_key, protocol_key, _) = create_test_keys(60);
        let attestation = SnapshotAttestation {
            block_height: 100,
            keysvals_hash: [0xABu8; BLAKE3_HASH_SIZE],
            authority_key: authority_key.clone(),
            protocol_key: protocol_key.clone(),
            signature: [3u8; ED25519_SIGNATURE_SIZE],
        };

        let serialized = serde_json::to_string(&attestation).expect("serialization should succeed");
        let deserialized: SnapshotAttestation =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        assert_eq!(deserialized.block_height, 100);
        assert_eq!(deserialized.keysvals_hash, [0xABu8; BLAKE3_HASH_SIZE]);
        assert_eq!(deserialized.authority_key, authority_key);
        assert_eq!(deserialized.protocol_key, protocol_key);
        assert_eq!(deserialized.signature, [3u8; ED25519_SIGNATURE_SIZE]);
    }

    #[test]
    fn test_snapshot_attestation_signing_message() {
        let keysvals_hash = [0xCDu8; BLAKE3_HASH_SIZE];
        let msg = SnapshotAttestation::signing_message(42, &keysvals_hash);

        // Verify the message format: block_height (8 bytes LE) || keysvals_hash (32 bytes)
        assert_eq!(msg.len(), SNAPSHOT_ATTESTATION_SIGNING_SIZE);
        assert_eq!(msg[0..8], 42u64.to_le_bytes());
        assert_eq!(msg[8..SNAPSHOT_ATTESTATION_SIGNING_SIZE], keysvals_hash);

        // Test with different values
        let msg2 = SnapshotAttestation::signing_message(1000, &keysvals_hash);
        assert_eq!(msg2[0..8], 1000u64.to_le_bytes());
        assert_eq!(msg2[8..SNAPSHOT_ATTESTATION_SIGNING_SIZE], keysvals_hash);

        // Different block heights should produce different messages
        assert_ne!(msg, msg2);
    }

    #[test]
    fn test_snapshot_attestation_message_bytes() {
        let (authority_key, protocol_key, _) = create_test_keys(61);
        let keysvals_hash = [0xEFu8; BLAKE3_HASH_SIZE];
        let attestation = SnapshotAttestation {
            block_height: 12345,
            keysvals_hash,
            authority_key,
            protocol_key,
            signature: [0u8; ED25519_SIGNATURE_SIZE],
        };

        let msg_bytes = attestation.message_bytes();
        let expected = SnapshotAttestation::signing_message(12345, &keysvals_hash);

        assert_eq!(msg_bytes, expected);
        assert_eq!(msg_bytes[0..8], 12345u64.to_le_bytes());
        assert_eq!(
            msg_bytes[8..SNAPSHOT_ATTESTATION_SIGNING_SIZE],
            keysvals_hash
        );
    }

    #[test]
    fn test_admin_transaction_snapshot_attestation_serialization() {
        let (authority_key, protocol_key, _) = create_test_keys(62);
        let attestation = SnapshotAttestation {
            block_height: 999,
            keysvals_hash: [0x12u8; BLAKE3_HASH_SIZE],
            authority_key: authority_key.clone(),
            protocol_key: protocol_key.clone(),
            signature: [0x78u8; ED25519_SIGNATURE_SIZE],
        };

        let admin_tx = AdminTransaction::SnapshotAttestation(Box::new(attestation.clone()));
        let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
        let deserialized: AdminTransaction =
            serde_json::from_str(&serialized).expect("deserialization should succeed");

        match deserialized {
            AdminTransaction::SnapshotAttestation(att) => {
                assert_eq!(att.block_height, 999);
                assert_eq!(att.keysvals_hash, [0x12u8; BLAKE3_HASH_SIZE]);
                assert_eq!(att.authority_key, authority_key);
                assert_eq!(att.protocol_key, protocol_key);
                assert_eq!(att.signature, [0x78u8; ED25519_SIGNATURE_SIZE]);
            }
            _ => panic!("Expected SnapshotAttestation"),
        }
    }
}
