rialo-types 0.18.1

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

//! Strongly-typed tokenomics epoch identifier.
//!
//! The tokenomics / inflation-reward epoch: the accounting period rewards are
//! distributed against. It is **one of four independent epoch counters**
//! (`ConsensusEpochId`, `TokenomicsEpochId`, `RexEpochId`, `DkgEpochId`) that
//! advance on unrelated schedules; a value of one is never interchangeable with
//! another, which is exactly why each is its own newtype rather than a shared
//! `Epoch`. Do not conflate this with the consensus epoch.
//!
//! It is `#[repr(transparent)]` and serializes identically to the underlying
//! `u64` (serde transparent; borsh of a single-field struct), so on-disk
//! `DBMap` keys/values and on-chain borsh encodings are byte-identical to the
//! bare `u64` they replace. Conversions are explicit only
//! ([`TokenomicsEpochId::new`] / [`TokenomicsEpochId::as_u64`]) — there is no
//! `From<u64>`/`Into<u64>`, matching the hardened [`BlockHeight`](crate::BlockHeight)
//! shape.

use std::fmt;

use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};

/// A tokenomics (inflation-reward) epoch number.
#[derive(
    BorshSerialize,
    BorshDeserialize,
    Clone,
    Copy,
    Debug,
    Default,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct TokenomicsEpochId(u64);

impl TokenomicsEpochId {
    /// The genesis tokenomics epoch.
    pub const ZERO: Self = Self(0);

    /// Wraps a raw tokenomics epoch number.
    pub const fn new(epoch: u64) -> Self {
        Self(epoch)
    }

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

impl fmt::Display for TokenomicsEpochId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Forward the caller's format flags (width/fill) to the inner `u64`.
        fmt::Display::fmt(&self.0, f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_as_u64_roundtrip() {
        assert_eq!(TokenomicsEpochId::new(42).as_u64(), 42);
        assert_eq!(TokenomicsEpochId::ZERO.as_u64(), 0);
    }

    #[test]
    fn test_display_forwards_format_flags() {
        assert_eq!(format!("{}", TokenomicsEpochId::new(42)), "42");
        assert_eq!(format!("{:04}", TokenomicsEpochId::new(42)), "0042");
    }

    // The indexer's `EpochRewardKey` is a RocksDB key serialized big-endian
    // fixint, and `epoch_metadata` is keyed by the epoch; both were bare `u64`.
    // Pin that `TokenomicsEpochId` is bincode-identical to `u64` (standalone and
    // as a struct field) so existing keys stay readable and ordered.
    #[test]
    fn test_bincode_matches_u64() {
        use bincode::Options;
        let cfg = bincode::DefaultOptions::new()
            .with_big_endian()
            .with_fixint_encoding();
        for raw in [0u64, 1, 42, u64::MAX] {
            assert_eq!(
                cfg.serialize(&TokenomicsEpochId::new(raw)).unwrap(),
                cfg.serialize(&raw).unwrap()
            );
        }
        // Ordering under the key codec must match numeric order.
        assert!(
            cfg.serialize(&TokenomicsEpochId::new(5)).unwrap()
                < cfg.serialize(&TokenomicsEpochId::new(6)).unwrap()
        );
        assert_eq!(
            bincode::serialize(&TokenomicsEpochId::new(9)).unwrap(),
            bincode::serialize(&9u64).unwrap()
        );
    }

    // Some `DBMap` values serialize with `bcs`; pin bcs-identity too.
    #[test]
    fn test_bcs_matches_u64() {
        let raw: u64 = 0xdead_beef;
        assert_eq!(
            bcs::to_bytes(&TokenomicsEpochId::new(raw)).unwrap(),
            bcs::to_bytes(&raw).unwrap()
        );
        assert_eq!(
            bcs::from_bytes::<TokenomicsEpochId>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
            TokenomicsEpochId::new(raw)
        );
    }

    // Borsh of a single-field struct is byte-identical to the inner `u64`, so
    // on-chain tokenomics encodings that adopt this type stay compatible.
    #[test]
    fn test_borsh_roundtrips_like_u64() {
        let e = TokenomicsEpochId::new(123);
        let bytes = borsh::to_vec(&e).unwrap();
        assert_eq!(bytes, 123u64.to_le_bytes());
        assert_eq!(borsh::from_slice::<TokenomicsEpochId>(&bytes).unwrap(), e);
    }
}