acropolis 0.2.1

UNDER CONSTRUCTION: A safe local-first Rust Cardano node scaffold.
Documentation
use crate::events::EventName;

pub mod selection;
use std::fmt;

pub const CHAIN_NEW_HEAD_EVENT: &str = "chain.new_head";
pub const CHAIN_FORK_EVENT: &str = "chain.fork";
pub const BLOCK_OFFERED_EVENT: &str = "chain.block_offered";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChainPoint {
    pub slot: u64,
    pub hash: [u8; 32],
}

impl ChainPoint {
    pub const ORIGIN: Self = Self {
        slot: 0,
        hash: [0; 32],
    };

    pub fn new(slot: u64, hash: [u8; 32]) -> Self {
        Self { slot, hash }
    }
}

impl fmt::Display for ChainPoint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "slot={} hash={}", self.slot, short_hash(&self.hash))
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChainTip {
    pub point: ChainPoint,
    pub bundle_number: u64,
}

impl ChainTip {
    pub const ORIGIN: Self = Self {
        point: ChainPoint::ORIGIN,
        bundle_number: 0,
    };

    pub fn new(point: ChainPoint, bundle_number: u64) -> Self {
        Self {
            point,
            bundle_number,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawBlock {
    pub point: ChainPoint,
    pub bundle_number: u64,
    pub bytes: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainHeadEvent {
    pub tip: ChainTip,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockOfferedEvent {
    pub block: RawBlock,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainRollbackEvent {
    pub from: ChainTip,
    pub to: ChainPoint,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainForkEvent {
    pub old_tip: ChainTip,
    pub candidate_tip: ChainTip,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RollbackJournalEntry {
    pub from: ChainTip,
    pub to: ChainPoint,
    pub reason: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RollbackJournal {
    entries: Vec<RollbackJournalEntry>,
}

impl RollbackJournal {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn record(
        &mut self,
        from: ChainTip,
        to: ChainPoint,
        reason: impl Into<String>,
    ) -> &RollbackJournalEntry {
        self.entries.push(RollbackJournalEntry {
            from,
            to,
            reason: reason.into(),
        });
        self.entries.last().expect("entry was just pushed")
    }

    pub fn entries(&self) -> &[RollbackJournalEntry] {
        &self.entries
    }

    pub fn last(&self) -> Option<&RollbackJournalEntry> {
        self.entries.last()
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForkCheck {
    SamePoint,
    SameSlotDifferentHash,
    DifferentSlot,
}

pub fn check_fork(left: ChainTip, right: ChainTip) -> ForkCheck {
    if left.point == right.point {
        ForkCheck::SamePoint
    } else if left.point.slot == right.point.slot {
        ForkCheck::SameSlotDifferentHash
    } else {
        ForkCheck::DifferentSlot
    }
}

#[derive(Debug, Clone)]
pub struct Chain {
    tip: ChainTip,
}

impl Default for Chain {
    fn default() -> Self {
        Self {
            tip: ChainTip::ORIGIN,
        }
    }
}

impl Chain {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn tip(&self) -> ChainTip {
        self.tip
    }

    pub fn apply_tip(&mut self, tip: ChainTip) -> Result<ChainHeadEvent, ChainError> {
        if tip.bundle_number < self.tip.bundle_number {
            return Err(ChainError::NonMonotonicBundleNumber {
                current: self.tip.bundle_number,
                candidate: tip.bundle_number,
            });
        }
        self.tip = tip;
        Ok(ChainHeadEvent { tip })
    }

    pub fn rollback_to(&mut self, point: ChainPoint) -> ChainRollbackEvent {
        let from = self.tip;
        self.tip = ChainTip::new(point, self.tip.bundle_number);
        ChainRollbackEvent { from, to: point }
    }

    pub fn new_event_name() -> EventName {
        EventName::new(CHAIN_NEW_HEAD_EVENT)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainError {
    NonMonotonicBundleNumber { current: u64, candidate: u64 },
}

impl fmt::Display for ChainError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NonMonotonicBundleNumber { current, candidate } => write!(
                f,
                "candidate bundle number {candidate} is behind current bundle number {current}"
            ),
        }
    }
}

impl std::error::Error for ChainError {}

fn short_hash(hash: &[u8; 32]) -> String {
    let mut out = String::with_capacity(8);
    for byte in &hash[..4] {
        out.push(nibble(byte >> 4));
        out.push(nibble(byte & 0x0f));
    }
    out
}

fn nibble(value: u8) -> char {
    match value {
        0..=9 => (b'0' + value) as char,
        _ => (b'a' + value - 10) as char,
    }
}

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

    #[test]
    fn chain_rejects_backward_block_number_without_paths() {
        let mut chain = Chain::new();
        let point = ChainPoint::new(10, [1; 32]);
        chain.apply_tip(ChainTip::new(point, 10)).unwrap();
        assert_eq!(
            chain.apply_tip(ChainTip::new(ChainPoint::new(11, [2; 32]), 9)),
            Err(ChainError::NonMonotonicBundleNumber {
                current: 10,
                candidate: 9,
            })
        );
    }

    #[test]
    fn rollback_journal_records_local_rollbacks() {
        let from = ChainTip::new(ChainPoint::new(10, [1; 32]), 10);
        let to = ChainPoint::new(8, [2; 32]);
        let mut journal = RollbackJournal::new();
        journal.record(from, to, "test");
        assert_eq!(journal.len(), 1);
        assert_eq!(journal.last().unwrap().to, to);
    }

    #[test]
    fn fork_check_detects_same_slot_hash_mismatch() {
        let left = ChainTip::new(ChainPoint::new(10, [1; 32]), 10);
        let right = ChainTip::new(ChainPoint::new(10, [2; 32]), 10);
        assert_eq!(check_fork(left, right), ForkCheck::SameSlotDifferentHash);
    }
}