rialo-types 0.4.1

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

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

use super::{
    BLAKE3_HASH_SIZE, ED25519_SIGNATURE_SIZE, READY_MESSAGE_INTENT_PREFIX,
    READY_MESSAGE_SIGNING_SIZE,
};
use crate::{
    consensus_crypto::{AuthorityPublicKey, NetworkPublicKey, ProtocolPublicKey},
    Multiaddr,
};

/// 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)
    }
}

/// 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 identity key (currently BLS12-381).
    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>,
}

/// 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)
    }
}