Skip to main content

qcoin_consensus/
lib.rs

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}