Skip to main content

entropa_node/
node.rs

1//! The node — one participant that holds a chain, a mempool, and a validator set, and
2//! runs the produce/accept consensus loop.
3//!
4//! Each round is seeded by the public randomness beacon. If this node is the beacon-
5//! selected proposer, it drains its mempool and produces the next block. Blocks from
6//! peers are accepted only if (a) the proposer is the correct beacon-selected validator
7//! for that block's beacon, and (b) the block passes full structural + post-quantum
8//! validation against the chain head.
9
10use entropa_core::{beacon, Block, Chain, ChainError, Probe, Transaction};
11
12use crate::consensus::{select_proposer, Validator};
13use crate::mempool::Mempool;
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum NodeError {
17    #[error("no validators configured")]
18    NoValidators,
19    #[error("block proposer is not the beacon-selected validator for this round")]
20    WrongProposer,
21    #[error(transparent)]
22    Invalid(#[from] ChainError),
23}
24
25/// A network participant.
26pub struct Node {
27    /// This node's post-quantum identity.
28    pub probe: Probe,
29    /// The validator set (shared, agreed configuration).
30    pub validators: Vec<Validator>,
31    /// This node's view of the chain.
32    pub chain: Chain,
33    /// Pending transactions.
34    pub mempool: Mempool,
35    /// Max transactions per block.
36    pub max_block_txs: usize,
37    /// A live beacon value fetched externally for the *current* round, if any (see
38    /// `entropa_core::beacon::sample_live`). Set this once per round before calling
39    /// `is_proposer`/`try_produce` so both see the identical value — fetching live
40    /// separately in each would risk straddling a beacon update between the two calls.
41    /// `None` falls back to the deterministic `beacon::sample(round)`.
42    pub live_beacon: Option<String>,
43}
44
45impl Node {
46    pub fn new(probe: Probe, validators: Vec<Validator>) -> Self {
47        Self {
48            probe,
49            validators,
50            chain: Chain::default(),
51            mempool: Mempool::new(),
52            max_block_txs: 64,
53            live_beacon: None,
54        }
55    }
56
57    /// Submit a transaction into this node's mempool.
58    pub fn submit(&mut self, tx: Transaction) {
59        self.mempool.submit(tx);
60    }
61
62    /// The beacon value for `round`: the injected live value if set, else the
63    /// deterministic offline fallback.
64    fn beacon_for(&self, round: u64) -> String {
65        self.live_beacon
66            .clone()
67            .unwrap_or_else(|| beacon::sample(round))
68    }
69
70    /// Am I the beacon-selected proposer for `round`?
71    pub fn is_proposer(&self, round: u64) -> bool {
72        let b = self.beacon_for(round);
73        select_proposer(&self.validators, &b)
74            .map(|i| self.validators[i].id == self.probe.id())
75            .unwrap_or(false)
76    }
77
78    /// If this node is the beacon-selected proposer for `round`, drain the mempool,
79    /// produce and append the next block, and return it for broadcast. Otherwise `None`.
80    pub fn try_produce(&mut self, round: u64, timestamp: u64) -> Option<Block> {
81        if !self.is_proposer(round) {
82            return None;
83        }
84        let b = self.beacon_for(round);
85        let txs = self.mempool.drain(self.max_block_txs);
86        let block = self.chain.draft(&self.probe, timestamp, b, txs);
87        self.chain
88            .try_append(block.clone())
89            .expect("self-drafted block is valid");
90        Some(block)
91    }
92
93    /// Accept a peer's block: enforce the consensus rule (correct proposer for the
94    /// block's beacon), then full structural + post-quantum validation.
95    pub fn accept(&mut self, block: Block) -> Result<(), NodeError> {
96        let idx =
97            select_proposer(&self.validators, &block.beacon).ok_or(NodeError::NoValidators)?;
98        if self.validators[idx].id != block.proposer_id {
99            return Err(NodeError::WrongProposer);
100        }
101        self.chain.try_append(block)?;
102        Ok(())
103    }
104
105    pub fn height(&self) -> usize {
106        self.chain.len()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    /// Build N nodes that share one validator set.
115    fn network(n: usize) -> Vec<Node> {
116        let probes: Vec<Probe> = (0..n).map(|_| Probe::spawn()).collect();
117        let validators: Vec<Validator> = probes
118            .iter()
119            .map(|p| Validator::new(p.id(), p.pubkey_hex()))
120            .collect();
121        probes
122            .into_iter()
123            .map(|p| Node::new(p, validators.clone()))
124            .collect()
125    }
126
127    #[test]
128    fn multi_node_consensus_agrees() {
129        let mut nodes = network(3);
130        // Give every node the same pending transaction each round.
131        for round in 0..6u64 {
132            for node in nodes.iter_mut() {
133                node.submit(Transaction::new(
134                    "user",
135                    "transfer",
136                    format!("round {round}"),
137                ));
138            }
139            let b = beacon::sample(round);
140            let sel = select_proposer(&nodes[0].validators, &b).unwrap();
141            let block = nodes[sel]
142                .try_produce(round, 1_000 + round)
143                .expect("the selected node produces");
144            for (i, node) in nodes.iter_mut().enumerate() {
145                if i != sel {
146                    node.accept(block.clone()).expect("peers accept");
147                }
148            }
149        }
150        // All nodes agree on height and each chain fully verifies.
151        let heights: Vec<usize> = nodes.iter().map(|n| n.height()).collect();
152        assert_eq!(heights, vec![6, 6, 6]);
153        let heads: Vec<String> = nodes
154            .iter()
155            .map(|n| n.chain.head().unwrap().hash.clone())
156            .collect();
157        assert!(heads.iter().all(|h| h == &heads[0]));
158        for node in &nodes {
159            assert_eq!(node.chain.verify(), Ok(()));
160        }
161    }
162
163    #[test]
164    fn rejects_block_from_wrong_proposer() {
165        let mut nodes = network(3);
166        let round = 0u64;
167        let b = beacon::sample(round);
168        let sel = select_proposer(&nodes[0].validators, &b).unwrap();
169        let wrong = (sel + 1) % 3;
170        // The wrong node self-drafts a block for this round (bypassing is_proposer).
171        let bad = nodes[wrong]
172            .chain
173            .draft(&nodes[wrong].probe, 42, b, Vec::new());
174        // A different node must reject it: wrong proposer for this beacon.
175        let victim = (sel + 2) % 3;
176        assert_eq!(nodes[victim].accept(bad), Err(NodeError::WrongProposer));
177    }
178
179    #[test]
180    fn only_one_proposer_per_round() {
181        let nodes = network(4);
182        for round in 0..12u64 {
183            let count = nodes.iter().filter(|n| n.is_proposer(round)).count();
184            assert_eq!(count, 1, "exactly one proposer selected per round");
185        }
186    }
187}