1use std::time::{SystemTime, UNIX_EPOCH};
2
3use blake3::Hasher;
4use qcoin_crypto::{
5 default_registry, InMemoryRegistry, PqSchemeRegistry, PqSignatureScheme, PrivateKey, PublicKey,
6 SignatureSchemeId,
7};
8use qcoin_ledger::ChainState;
9use qcoin_script::DeterministicScriptEngine;
10use qcoin_types::{consensus_codec, Block, Hash256, Transaction};
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14pub enum ConsensusError {
15 #[error("invalid block")]
16 InvalidBlock,
17 #[error("signature verification failed")]
18 SignatureError,
19 #[error("ledger error: {0}")]
20 LedgerError(String),
21 #[error("other consensus error: {0}")]
22 Other(String),
23}
24
25pub trait ValidatorIdentity {
26 fn public_key(&self) -> &PublicKey;
27}
28
29pub trait ConsensusEngine {
30 fn propose_block(
31 &self,
32 chain: &ChainState,
33 txs: Vec<Transaction>,
34 ) -> Result<Block, ConsensusError>;
35
36 fn validate_block(&self, chain: &ChainState, block: &Block) -> Result<(), ConsensusError>;
37}
38
39pub struct DummyConsensusEngine {
40 registry: InMemoryRegistry,
41 signing_scheme: SignatureSchemeId,
42 signing_key: PrivateKey,
43 public_key: PublicKey,
44 validators: Vec<PublicKey>,
45}
46
47impl Default for DummyConsensusEngine {
48 fn default() -> Self {
49 let registry = default_registry();
50 Self::new(registry, SignatureSchemeId::Dilithium2)
51 }
52}
53
54impl DummyConsensusEngine {
55 pub fn new(registry: InMemoryRegistry, signing_scheme: SignatureSchemeId) -> Self {
56 let (public_key, signing_key) = {
57 let scheme = registry
58 .get(&signing_scheme)
59 .expect("signing scheme must be registered for dummy consensus");
60 scheme
61 .keygen()
62 .expect("key generation must succeed for dummy consensus")
63 };
64 let validators = vec![public_key.clone()];
65
66 Self {
67 registry,
68 signing_scheme,
69 signing_key,
70 public_key,
71 validators,
72 }
73 }
74
75 pub fn with_validators(
76 registry: InMemoryRegistry,
77 signing_scheme: SignatureSchemeId,
78 validators: Vec<PublicKey>,
79 ) -> Self {
80 let mut engine = Self::new(registry, signing_scheme);
81
82 if validators.is_empty() {
83 engine.validators.push(engine.public_key.clone());
84 } else {
85 engine.validators = validators;
86 }
87
88 engine
89 }
90
91 pub fn from_keys(
92 registry: InMemoryRegistry,
93 public_key: PublicKey,
94 signing_key: PrivateKey,
95 validators: Vec<PublicKey>,
96 ) -> Result<Self, ConsensusError> {
97 if public_key.scheme != signing_key.scheme {
98 return Err(ConsensusError::Other(
99 "public/private key scheme mismatch".to_string(),
100 ));
101 }
102
103 let mut effective_validators = validators;
104 if effective_validators.is_empty() {
105 effective_validators.push(public_key.clone());
106 }
107
108 Ok(Self {
109 registry,
110 signing_scheme: public_key.scheme,
111 signing_key,
112 public_key,
113 validators: effective_validators,
114 })
115 }
116
117 fn scheme(&self, id: &SignatureSchemeId) -> Option<&dyn PqSignatureScheme> {
118 self.registry.get(id)
119 }
120
121 fn expected_proposer(&self, height: u64) -> Result<&PublicKey, ConsensusError> {
122 if self.validators.is_empty() {
123 return Err(ConsensusError::Other("validator set is empty".to_string()));
124 }
125
126 let index = ((height - 1) as usize) % self.validators.len();
127 self.validators
128 .get(index)
129 .ok_or_else(|| ConsensusError::Other("invalid proposer index".to_string()))
130 }
131}
132
133fn compute_tx_root(txs: &[Transaction]) -> Hash256 {
134 let mut hasher = Hasher::new();
135
136 for tx in txs {
137 let tx_id = tx.tx_id();
138 hasher.update(&tx_id);
139 }
140
141 *hasher.finalize().as_bytes()
142}
143
144fn compute_state_root(
145 chain: &ChainState,
146 txs: &[Transaction],
147 height: u64,
148) -> Result<Hash256, ConsensusError> {
149 let mut ledger = chain.ledger.clone();
150 let script_engine = DeterministicScriptEngine::default();
151
152 for tx in txs {
153 ledger
154 .apply_transaction(tx, &script_engine, height, chain.chain_id)
155 .map_err(|err| ConsensusError::LedgerError(err.to_string()))?;
156 }
157
158 Ok(ledger.state_root())
159}
160
161fn current_unix_timestamp() -> Result<u64, ConsensusError> {
162 let now = SystemTime::now()
163 .duration_since(UNIX_EPOCH)
164 .map_err(|err| ConsensusError::Other(format!("failed to read time: {err}")))?;
165 Ok(now.as_secs())
166}
167
168impl ConsensusEngine for DummyConsensusEngine {
169 fn propose_block(
170 &self,
171 chain: &ChainState,
172 txs: Vec<Transaction>,
173 ) -> Result<Block, ConsensusError> {
174 let next_height = chain.height + 1;
175 let expected_proposer = self.expected_proposer(next_height)?;
176
177 if *expected_proposer != self.public_key {
178 return Err(ConsensusError::InvalidBlock);
179 }
180
181 let state_root = compute_state_root(chain, &txs, next_height)?;
182 let tx_root = compute_tx_root(&txs);
183 let timestamp = current_unix_timestamp()?;
184
185 let header = qcoin_types::BlockHeader {
186 parent_hash: chain.tip_hash,
187 state_root,
188 tx_root,
189 height: next_height,
190 timestamp,
191 };
192
193 let header_bytes = consensus_codec::encode_block_header(&header);
194
195 let signature = self
196 .scheme(&self.signing_scheme)
197 .expect("signing scheme must be available")
198 .sign(&self.signing_key, &header_bytes)
199 .map_err(|_| ConsensusError::SignatureError)?;
200
201 Ok(Block {
202 header,
203 transactions: txs,
204 proposer_public_key: self.public_key.clone(),
205 signature,
206 })
207 }
208
209 fn validate_block(&self, chain: &ChainState, block: &Block) -> Result<(), ConsensusError> {
210 if block.header.height != chain.height + 1 {
211 return Err(ConsensusError::InvalidBlock);
212 }
213
214 if block.header.parent_hash != chain.tip_hash {
215 return Err(ConsensusError::InvalidBlock);
216 }
217
218 if block.header.timestamp <= chain.last_timestamp {
219 return Err(ConsensusError::InvalidBlock);
220 }
221
222 let expected_proposer = self.expected_proposer(block.header.height)?;
223 if block.proposer_public_key != *expected_proposer {
224 return Err(ConsensusError::InvalidBlock);
225 }
226
227 let expected_tx_root = compute_tx_root(&block.transactions);
228 if block.header.tx_root != expected_tx_root {
229 return Err(ConsensusError::InvalidBlock);
230 }
231
232 let expected_state_root =
233 compute_state_root(chain, &block.transactions, block.header.height)?;
234 if block.header.state_root != expected_state_root {
235 return Err(ConsensusError::InvalidBlock);
236 }
237
238 let header_bytes = consensus_codec::encode_block_header(&block.header);
239
240 let scheme = self
241 .scheme(&block.signature.scheme)
242 .ok_or(ConsensusError::SignatureError)?;
243
244 scheme
245 .verify(&block.proposer_public_key, &header_bytes, &block.signature)
246 .map_err(|_| ConsensusError::SignatureError)?;
247
248 Ok(())
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use qcoin_crypto::SignatureSchemeId;
256 use qcoin_types::TransactionKind;
257
258 #[test]
259 fn validate_block_rejects_mutated_transactions() {
260 let engine = DummyConsensusEngine::default();
261 let chain = ChainState::default();
262
263 let tx = Transaction {
264 core: qcoin_types::TransactionCore {
265 kind: TransactionKind::Transfer,
266 inputs: Vec::new(),
267 outputs: Vec::new(),
268 },
269 witness: qcoin_types::TransactionWitness::default(),
270 };
271
272 let mut block = engine
273 .propose_block(&chain, vec![tx.clone()])
274 .expect("block should be proposed");
275
276 engine
277 .validate_block(&chain, &block)
278 .expect("freshly built block should validate");
279
280 block.transactions.push(tx);
281
282 let result = engine.validate_block(&chain, &block);
283 assert!(matches!(result, Err(ConsensusError::InvalidBlock)));
284 }
285
286 #[test]
287 fn validate_block_rejects_wrong_parent_hash() {
288 let engine = DummyConsensusEngine::default();
289 let chain = ChainState::default();
290
291 let block = engine
292 .propose_block(&chain, Vec::new())
293 .expect("block should build");
294
295 let mut forked_chain = chain.clone();
296 forked_chain.tip_hash = [7u8; 32];
297
298 let result = engine.validate_block(&forked_chain, &block);
299 assert!(matches!(result, Err(ConsensusError::InvalidBlock)));
300 }
301
302 #[test]
303 fn validate_block_rejects_bad_signature() {
304 let engine = DummyConsensusEngine::default();
305 let chain = ChainState::default();
306
307 let tx = Transaction {
308 core: qcoin_types::TransactionCore {
309 kind: TransactionKind::Transfer,
310 inputs: Vec::new(),
311 outputs: Vec::new(),
312 },
313 witness: qcoin_types::TransactionWitness::default(),
314 };
315
316 let mut block = engine
317 .propose_block(&chain, vec![tx])
318 .expect("block should build");
319
320 if let Some(byte) = block.signature.bytes.first_mut() {
321 *byte ^= 0xFF;
322 } else {
323 block.signature.bytes.push(1);
324 }
325
326 let result = engine.validate_block(&chain, &block);
327 assert!(matches!(result, Err(ConsensusError::SignatureError)));
328 }
329
330 #[test]
331 fn validate_block_rejects_block_from_unexpected_proposer() {
332 let mut engine = DummyConsensusEngine::with_validators(
333 default_registry(),
334 SignatureSchemeId::Dilithium2,
335 Vec::new(),
336 );
337 let alternate_engine =
338 DummyConsensusEngine::new(default_registry(), SignatureSchemeId::Dilithium2);
339
340 engine.validators = vec![
341 engine.public_key.clone(),
342 alternate_engine.public_key.clone(),
343 ];
344
345 let chain = ChainState::default();
346 let block = engine
347 .propose_block(&chain, Vec::new())
348 .expect("block should be proposed");
349
350 let mut wrong_proposer_block = block.clone();
351 wrong_proposer_block.proposer_public_key = alternate_engine.public_key.clone();
352 let header_bytes = consensus_codec::encode_block_header(&wrong_proposer_block.header);
353 wrong_proposer_block.signature = alternate_engine
354 .scheme(&alternate_engine.signing_scheme)
355 .expect("scheme should exist")
356 .sign(&alternate_engine.signing_key, &header_bytes)
357 .expect("signing should succeed");
358
359 let result = engine.validate_block(&chain, &wrong_proposer_block);
360 assert!(matches!(result, Err(ConsensusError::InvalidBlock)));
361 }
362
363 #[test]
364 fn validate_block_rejects_tampered_state_root() {
365 let engine = DummyConsensusEngine::default();
366 let chain = ChainState::default();
367
368 let block = engine
369 .propose_block(&chain, Vec::new())
370 .expect("block should be proposed");
371
372 let mut tampered = block.clone();
373 tampered.header.state_root = [9u8; 32];
374
375 let header_bytes = consensus_codec::encode_block_header(&tampered.header);
376 tampered.signature = engine
377 .scheme(&engine.signing_scheme)
378 .expect("scheme should exist")
379 .sign(&engine.signing_key, &header_bytes)
380 .expect("signing should succeed");
381
382 let result = engine.validate_block(&chain, &tampered);
383 assert!(matches!(result, Err(ConsensusError::InvalidBlock)));
384 }
385}