1use 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
25pub struct Node {
27 pub probe: Probe,
29 pub validators: Vec<Validator>,
31 pub chain: Chain,
33 pub mempool: Mempool,
35 pub max_block_txs: usize,
37 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 pub fn submit(&mut self, tx: Transaction) {
59 self.mempool.submit(tx);
60 }
61
62 fn beacon_for(&self, round: u64) -> String {
65 self.live_beacon
66 .clone()
67 .unwrap_or_else(|| beacon::sample(round))
68 }
69
70 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 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 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 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 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 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 let bad = nodes[wrong]
172 .chain
173 .draft(&nodes[wrong].probe, 42, b, Vec::new());
174 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}