Skip to main content

micronet_antenna_core/
state.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use crate::{Decision, Message, NodeId, Proposal, ProposalId, Vote, VoteRule};
5
6#[derive(Clone, Debug, Default)]
7/// Deterministic replicated state.
8///
9/// This struct represents the "constitution" / shared truth a node derives
10/// by applying messages through [`Runtime`].
11pub struct GlobalState {
12    peers: BTreeSet<NodeId>,
13    proposals: BTreeMap<ProposalId, Proposal>,
14    votes: BTreeMap<ProposalId, Vec<Vote>>,
15    decisions: BTreeMap<ProposalId, Decision>,
16}
17
18impl GlobalState {
19    /// Returns the current known peers (citizens).
20    pub fn peers(&self) -> &BTreeSet<NodeId> {
21        &self.peers
22    }
23
24    /// Returns all known proposals.
25    pub fn proposals(&self) -> &BTreeMap<ProposalId, Proposal> {
26        &self.proposals
27    }
28
29    /// Returns the current derived decision for a proposal.
30    pub fn decision(&self, id: ProposalId) -> Option<Decision> {
31        self.decisions.get(&id).copied()
32    }
33}
34
35#[derive(Clone, Debug)]
36/// Side effects emitted by [`Runtime::apply`].
37///
38/// Events are intended for UI/logging/telemetry or for driving higher-layer reactions.
39pub enum RuntimeEvent {
40    /// A new peer became known.
41    PeerDiscovered(NodeId),
42    /// A proposal was observed.
43    ProposalReceived(ProposalId),
44    /// A vote was observed.
45    VoteReceived(ProposalId),
46    /// The derived decision for a proposal changed.
47    DecisionUpdated {
48        proposal_id: ProposalId,
49        decision: Decision,
50    },
51}
52
53#[derive(Clone, Debug)]
54/// Execution engine that applies [`Message`] values to derive [`GlobalState`].
55///
56/// The runtime is intentionally small:
57///
58/// - it owns a node identity
59/// - it owns state
60/// - it provides a deterministic transition function (`apply`)
61pub struct Runtime {
62    node_id: NodeId,
63    state: GlobalState,
64    vote_rule: VoteRule,
65}
66
67impl Default for Runtime {
68    fn default() -> Self {
69        Self::new(NodeId::new([0u8; 32]))
70    }
71}
72
73impl Runtime {
74    /// Creates a new runtime with the provided node identity.
75    pub fn new(node_id: NodeId) -> Self {
76        Self {
77            node_id,
78            state: GlobalState::default(),
79            vote_rule: VoteRule::SimpleMajority,
80        }
81    }
82
83    #[cfg(feature = "std")]
84    /// Convenience constructor that generates a random node ID.
85    pub fn new_random() -> Self {
86        Self::new(NodeId::random())
87    }
88
89    /// Returns this node's identity.
90    pub fn node_id(&self) -> NodeId {
91        self.node_id
92    }
93
94    /// Returns a shared reference to the derived global state.
95    pub fn state(&self) -> &GlobalState {
96        &self.state
97    }
98
99    /// Inserts a proposal locally.
100    ///
101    /// This is a local helper; in a real network you typically broadcast a `Message::Proposal`.
102    pub fn submit_proposal(&mut self, proposal: Proposal) {
103        let id = proposal.id;
104        self.state.proposals.insert(id, proposal);
105        self.state.decisions.insert(id, Decision::Pending);
106    }
107
108    /// Applies a single message and returns the resulting events.
109    ///
110    /// This function is the heart of the system: all replicas should call `apply` for every
111    /// received message (in a consistent order if you require strong determinism).
112    pub fn apply(&mut self, msg: Message) -> Vec<RuntimeEvent> {
113        let mut out = Vec::new();
114
115        match msg {
116            Message::Hello { node } | Message::Heartbeat { node } => {
117                if self.state.peers.insert(node) {
118                    out.push(RuntimeEvent::PeerDiscovered(node));
119                }
120            }
121            Message::Proposal(p) => {
122                let id = p.id;
123                self.state.proposals.entry(id).or_insert(p);
124                self.state.decisions.entry(id).or_insert(Decision::Pending);
125                out.push(RuntimeEvent::ProposalReceived(id));
126            }
127            Message::Vote { from: _, vote } => {
128                let pid = vote.proposal_id;
129                self.state.votes.entry(pid).or_default().push(vote);
130                out.push(RuntimeEvent::VoteReceived(pid));
131            }
132        }
133
134        self.recompute_decisions(&mut out);
135        out
136    }
137
138    fn recompute_decisions(&mut self, out: &mut Vec<RuntimeEvent>) {
139        // Higher layers may define eligibility differently. For now, we consider
140        // "known peers" as eligible voters. We also clamp to at least 1 to avoid
141        // division-by-zero.
142        let eligible = self.state.peers.len().max(1);
143
144        let proposal_ids: Vec<ProposalId> = self.state.proposals.keys().copied().collect();
145        for pid in proposal_ids {
146            let votes = self
147                .state
148                .votes
149                .get(&pid)
150                .map(|v| v.as_slice())
151                .unwrap_or(&[]);
152            let decision = self.vote_rule.decide(pid, votes, eligible);
153
154            let prev = self.state.decisions.get(&pid).copied();
155            if prev != Some(decision) {
156                self.state.decisions.insert(pid, decision);
157                out.push(RuntimeEvent::DecisionUpdated {
158                    proposal_id: pid,
159                    decision,
160                });
161            }
162        }
163    }
164}