yedad_common 0.2.0

Shared types, constants, and utilities for Yedad network
Documentation
//! # yedad_common
//!
//! Shared types, constants, and utilities for the Yedad network.
//! This crate is the foundation for all other Yedad crates (`yedad_key`, `yedad_core`, `yedad_wallet`).
//!
//! It provides:
//! - Core data structures: [`Slot`], [`SlotKey`], [`Transaction`], [`TransactionRecord`], [`NotepadEntry`]
//! - Type aliases: [`Address`] (12 bytes), [`Hash`] (32 bytes), [`NanoYD`]
//! - Economic constants (supply, halving, rewards)
//! - Error types [`YedadError`]
//! - Helper functions for address conversion

use serde::{Deserialize, Serialize};
use thiserror::Error;

// ============================================================================
// Constants
// ============================================================================

/// Size of an address in bytes (12 bytes)
pub const ADDRESS_SIZE: usize = 12;

/// Size of a hash in bytes (32 bytes)
pub const HASH_SIZE: usize = 32;

/// Size of a serialized slot (64 bytes)
pub const SLOT_SIZE: usize = 64;

/// Pulse interval in milliseconds (1.5 seconds)
pub const PULSE_INTERVAL_MS: u64 = 1500;

/// Default ladder height (number of steps)
pub const DEFAULT_LADDER_HEIGHT: usize = 100_000;

// ============================================================================
// Coin & Economics
// ============================================================================

/// Number of decimal places for YD (8, like Bitcoin satoshis)
pub const COIN_DECIMALS: u32 = 8;

/// One YD in nanoYD (100,000,000 nanoYD)
pub const COIN: u64 = 10_u64.pow(COIN_DECIMALS);

/// Maximum total supply in nanoYD (19 million YD)
pub const MAX_SUPPLY: u64 = 19_000_000 * COIN;

/// Number of pulses between halvings (≈4 years at 1.5s/pulse)
pub const HALVING_PULSES: u64 = 84_000_000;

/// Genesis reward per pulse (in nanoYD)
pub const GENESIS_REWARD: u64 = MAX_SUPPLY / 2 / HALVING_PULSES;

// ============================================================================
// Type Aliases
// ============================================================================

/// A 12‑byte address (user or node identifier)
pub type Address = [u8; ADDRESS_SIZE];

/// A 32‑byte hash (SHA‑256 output)
pub type Hash = [u8; HASH_SIZE];

/// Amount in nanoYD (1 YD = 10^8 nanoYD)
pub type NanoYD = u64;

// ============================================================================
// Slot
// ============================================================================

/// A slot represents the state of a user or node in the Yedad network.
/// It contains all information needed to validate transactions and synchronize state.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct Slot {
    /// Current pulse ID when this slot was last updated
    pub pulse_id: u32,
    /// User address (12 bytes)
    pub user_address: Address,
    /// Current ladder identifier (incremented when ladder is exhausted)
    pub ladder_id: u32,
    /// Current step position in the ladder (remaining steps)
    pub step_number: u32,
    /// Balance in nanoYD
    pub balance: NanoYD,
    /// Last hash (current top of the ladder)
    pub last_hash: Hash,
}

impl Slot {
    /// Create a new slot with default values (pulse_id = 0, balance = 0).
    pub fn new(user_address: Address, ladder_id: u32, step_number: u32, last_hash: Hash) -> Self {
        Self {
            pulse_id: 0,
            user_address,
            ladder_id,
            step_number,
            balance: 0,
            last_hash,
        }
    }

    /// Serialize the slot into a fixed-size byte array (64 bytes).
    pub fn to_bytes(&self) -> [u8; SLOT_SIZE] {
        let mut bytes = [0u8; SLOT_SIZE];
        bytes[0..4].copy_from_slice(&self.pulse_id.to_le_bytes());
        bytes[4..16].copy_from_slice(&self.user_address);
        bytes[16..20].copy_from_slice(&self.ladder_id.to_le_bytes());
        bytes[20..24].copy_from_slice(&self.step_number.to_le_bytes());
        bytes[24..32].copy_from_slice(&self.balance.to_le_bytes());
        bytes[32..64].copy_from_slice(&self.last_hash);
        bytes
    }

    /// Deserialize a slot from a fixed-size byte array (64 bytes).
    pub fn from_bytes(bytes: &[u8; SLOT_SIZE]) -> Self {
        let pulse_id = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
        let mut user_address = [0u8; ADDRESS_SIZE];
        user_address.copy_from_slice(&bytes[4..16]);
        let ladder_id = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
        let step_number = u32::from_le_bytes(bytes[20..24].try_into().unwrap());
        let balance = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
        let mut last_hash = [0u8; HASH_SIZE];
        last_hash.copy_from_slice(&bytes[32..64]);
        Self {
            pulse_id,
            user_address,
            ladder_id,
            step_number,
            balance,
            last_hash,
        }
    }
}

// ============================================================================
// SlotKey (registration key)
// ============================================================================

/// A lightweight key used to register a new slot in the network.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotKey {
    /// User address derived from the private key
    pub user_address: Address,
    /// Initial ladder ID (usually 1)
    pub ladder_id: u32,
    /// Last hash (top of the ladder)
    pub last_hash: Hash,
}

// ============================================================================
// Transaction
// ============================================================================

/// A transaction submitted by a wallet to the network.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
    /// Timestamp (Unix seconds or pulse number)
    pub timestamp: u32,
    /// Sender address
    pub from: Address,
    /// Recipient address
    pub to: Address,
    /// Amount in nanoYD
    pub amount: NanoYD,
    /// Cryptographic proof (the next ladder step)
    pub proof: Hash,
}

// ============================================================================
// TransactionRecord (history entry)
// ============================================================================

/// A record of a transaction stored in a node's history (without proof).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionRecord {
    /// Sender address
    pub from: Address,
    /// Recipient address
    pub to: Address,
    /// Timestamp (Unix seconds or pulse number)
    pub timestamp: u32,
    /// Amount in nanoYD
    pub amount: NanoYD,
}

// ============================================================================
// Notepad Entry
// ============================================================================

/// An entry in the global notepad (append‑only pulse log).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotepadEntry {
    /// Pulse ID (sequential)
    pub pulse_id: u32,
    /// Unix timestamp in milliseconds
    pub timestamp: u64,
    /// Root hash of all slots at this pulse
    pub root_hash: Hash,
    /// Compact 4‑byte hash (e.g., CRC32 of the combined state)
    pub pulse_hash: u32,
}

// ============================================================================
// Errors
// ============================================================================

/// Common error type used across all Yedad crates.
#[derive(Error, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum YedadError {
    /// The provided proof is invalid (double-hash mismatch)
    #[error("Invalid proof")]
    InvalidProof,

    /// Insufficient balance for the transaction
    #[error("Insufficient balance")]
    InsufficientBalance,

    /// The requested account was not found
    #[error("Account not found")]
    AccountNotFound,

    /// The ladder has been exhausted (no more steps left)
    #[error("Ladder exhausted")]
    LadderExhausted,

    /// Arithmetic overflow (balance addition/subtraction)
    #[error("Arithmetic overflow")]
    ArithmeticOverflow,

    /// The account already exists (when trying to register an existing slot)
    #[error("Already exists")]
    AlreadyExists,

    /// Generic I/O error (with message)
    #[error("IO error: {0}")]
    Io(String),
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Convert an address to a hex string.
pub fn address_to_string(address: &Address) -> String {
    hex::encode(address)
}

/// Convert a hex string to an address.
pub fn string_to_address(s: &str) -> Result<Address, hex::FromHexError> {
    let bytes = hex::decode(s)?;
    let mut addr = [0u8; ADDRESS_SIZE];
    addr.copy_from_slice(&bytes);
    Ok(addr)
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_slot_serialization() {
        let addr = [1u8; 12];
        let last_hash = [2u8; 32];
        let slot = Slot::new(addr, 1, 100_000, last_hash);
        let bytes = slot.to_bytes();
        let restored = Slot::from_bytes(&bytes);
        assert_eq!(slot.user_address, restored.user_address);
        assert_eq!(slot.last_hash, restored.last_hash);
        assert_eq!(slot.balance, restored.balance);
    }

    #[test]
    fn test_address_conversion() {
        let addr = [
            0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
        ];
        let s = address_to_string(&addr);
        let restored = string_to_address(&s).unwrap();
        assert_eq!(addr, restored);
    }
}