1use crate::block::{block_digest, Block, Transaction};
14use crate::pqc::{probe_id, verify_hex, Probe};
15
16pub const BIG_BANG: &str = "BIGBANG";
18
19#[derive(Debug, thiserror::Error, PartialEq, Eq)]
20pub enum ChainError {
21 #[error("block {0}: index out of sequence")]
22 BadIndex(u64),
23 #[error("block {0}: prev_hash does not link to the previous block")]
24 BrokenLink(u64),
25 #[error("block {0}: hash does not match recomputed digest")]
26 BadHash(u64),
27 #[error("block {0}: proposer_id does not match proposer_pubkey")]
28 ForgedProposer(u64),
29 #[error("block {0}: post-quantum signature is invalid")]
30 BadSignature(u64),
31 #[error("chain is empty")]
32 Empty,
33}
34
35#[derive(Debug, Default, Clone)]
37pub struct Chain {
38 pub blocks: Vec<Block>,
39}
40
41impl Chain {
42 pub fn genesis(founder: &Probe, timestamp: u64, beacon: impl Into<String>) -> Self {
44 let mut chain = Chain { blocks: Vec::new() };
45 chain.propose(founder, timestamp, beacon, Vec::new());
46 chain
47 }
48
49 pub fn draft(
54 &self,
55 proposer: &Probe,
56 timestamp: u64,
57 beacon: impl Into<String>,
58 transactions: Vec<Transaction>,
59 ) -> Block {
60 let index = self.blocks.len() as u64;
61 let prev_hash = self
62 .blocks
63 .last()
64 .map(|b| b.hash.clone())
65 .unwrap_or_else(|| BIG_BANG.to_string());
66 let beacon = beacon.into();
67 let proposer_id = proposer.id();
68 let proposer_pubkey = proposer.pubkey_hex();
69 let digest = block_digest(
70 index,
71 timestamp,
72 &prev_hash,
73 &beacon,
74 &transactions,
75 &proposer_id,
76 );
77 let signature = proposer.sign_hex(&digest);
78 Block {
79 index,
80 timestamp,
81 prev_hash,
82 beacon,
83 transactions,
84 proposer_id,
85 proposer_pubkey,
86 hash: hex::encode(digest),
87 signature,
88 }
89 }
90
91 pub fn try_append(&mut self, block: Block) -> Result<(), ChainError> {
95 let expected = self.blocks.len() as u64;
96 let prev = self
97 .blocks
98 .last()
99 .map(|b| b.hash.as_str())
100 .unwrap_or(BIG_BANG);
101 if block.index != expected {
102 return Err(ChainError::BadIndex(expected));
103 }
104 if block.prev_hash != prev {
105 return Err(ChainError::BrokenLink(expected));
106 }
107 if probe_id(&block.proposer_pubkey) != block.proposer_id {
108 return Err(ChainError::ForgedProposer(expected));
109 }
110 let digest = block_digest(
111 block.index,
112 block.timestamp,
113 &block.prev_hash,
114 &block.beacon,
115 &block.transactions,
116 &block.proposer_id,
117 );
118 if hex::encode(digest) != block.hash {
119 return Err(ChainError::BadHash(expected));
120 }
121 if !verify_hex(&block.proposer_pubkey, &digest, &block.signature) {
122 return Err(ChainError::BadSignature(expected));
123 }
124 self.blocks.push(block);
125 Ok(())
126 }
127
128 pub fn propose(
130 &mut self,
131 proposer: &Probe,
132 timestamp: u64,
133 beacon: impl Into<String>,
134 transactions: Vec<Transaction>,
135 ) -> u64 {
136 let block = self.draft(proposer, timestamp, beacon, transactions);
137 let index = block.index;
138 self.try_append(block)
139 .expect("a self-drafted block is always valid");
140 index
141 }
142
143 pub fn verify(&self) -> Result<(), ChainError> {
145 if self.blocks.is_empty() {
146 return Err(ChainError::Empty);
147 }
148 let mut prev = BIG_BANG.to_string();
149 for (i, b) in self.blocks.iter().enumerate() {
150 let i = i as u64;
151 if b.index != i {
152 return Err(ChainError::BadIndex(i));
153 }
154 if b.prev_hash != prev {
155 return Err(ChainError::BrokenLink(i));
156 }
157 if probe_id(&b.proposer_pubkey) != b.proposer_id {
158 return Err(ChainError::ForgedProposer(i));
159 }
160 let digest = block_digest(
161 b.index,
162 b.timestamp,
163 &b.prev_hash,
164 &b.beacon,
165 &b.transactions,
166 &b.proposer_id,
167 );
168 if hex::encode(digest) != b.hash {
169 return Err(ChainError::BadHash(i));
170 }
171 if !verify_hex(&b.proposer_pubkey, &digest, &b.signature) {
172 return Err(ChainError::BadSignature(i));
173 }
174 prev = b.hash.clone();
175 }
176 Ok(())
177 }
178
179 pub fn len(&self) -> usize {
180 self.blocks.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
184 self.blocks.is_empty()
185 }
186
187 pub fn head(&self) -> Option<&Block> {
188 self.blocks.last()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::beacon;
196
197 fn sample_chain() -> (Chain, Probe, Probe) {
198 let founder = Probe::spawn();
199 let alice = Probe::spawn();
200 let mut chain = Chain::genesis(&founder, 1_000, beacon::sample(0));
201 chain.propose(
202 &alice,
203 1_001,
204 beacon::sample(1),
205 vec![Transaction::new(
206 alice.id(),
207 "transfer",
208 "10 -> PROBE-DEADBEEF",
209 )],
210 );
211 chain.propose(
212 &founder,
213 1_002,
214 beacon::sample(2),
215 vec![Transaction::new(
216 "oracle",
217 "attest",
218 "cosmic beacon round 2",
219 )],
220 );
221 (chain, founder, alice)
222 }
223
224 #[test]
225 fn builds_and_verifies() {
226 let (chain, _, _) = sample_chain();
227 assert_eq!(chain.len(), 3);
228 assert_eq!(chain.verify(), Ok(()));
229 }
230
231 #[test]
232 fn detects_tampered_transaction() {
233 let (mut chain, _, _) = sample_chain();
234 chain.blocks[1].transactions[0].payload = "9000 -> PROBE-ATTACKER".into();
235 assert_eq!(chain.verify(), Err(ChainError::BadHash(1)));
236 }
237
238 #[test]
239 fn detects_forged_signature() {
240 let (mut chain, _, _) = sample_chain();
241 let attacker = Probe::spawn();
242 let digest = block_digest(
243 chain.blocks[2].index,
244 chain.blocks[2].timestamp,
245 &chain.blocks[2].prev_hash,
246 &chain.blocks[2].beacon,
247 &chain.blocks[2].transactions,
248 &chain.blocks[2].proposer_id,
249 );
250 chain.blocks[2].signature = attacker.sign_hex(&digest);
251 assert_eq!(chain.verify(), Err(ChainError::BadSignature(2)));
252 }
253
254 #[test]
255 fn detects_broken_link() {
256 let (mut chain, _, _) = sample_chain();
257 chain.blocks[1].prev_hash = "0".repeat(64);
258 assert_eq!(chain.verify(), Err(ChainError::BrokenLink(1)));
259 }
260
261 #[test]
262 fn try_append_rejects_foreign_block() {
263 let (mut chain, _, _) = sample_chain();
265 let rogue = Probe::spawn();
266 let foreign = Chain::default().draft(&rogue, 5, beacon::sample(9), Vec::new());
267 assert_eq!(chain.try_append(foreign), Err(ChainError::BadIndex(3)));
268 }
269
270 #[test]
271 fn serde_round_trip() {
272 let (chain, _, _) = sample_chain();
273 let json = serde_json::to_string(&chain.blocks).unwrap();
274 let back: Vec<Block> = serde_json::from_str(&json).unwrap();
275 assert_eq!(back.len(), 3);
276 }
277}