1use crate::crypto::Sha256;
10
11pub const GENESIS_PREV_HASH: [u8; 32] = [0u8; 32];
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SignedFields {
20 pub signer_pubkey: [u8; 32],
21 pub signature: Vec<u8>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Block {
29 pub block_height: u64,
30 pub prev_hash: [u8; 32],
31 pub timestamp_ms: u64,
32 pub payload: Vec<u8>,
33 pub signed: Option<SignedFields>,
34 pub hash: [u8; 32],
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ChainTip {
40 pub block_height: u64,
41 pub hash: [u8; 32],
42 pub timestamp_ms: u64,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum BlockchainError {
48 ConflictRetry { expected: [u8; 32], got: [u8; 32] },
51 Immutable,
54}
55
56impl std::fmt::Display for BlockchainError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Self::ConflictRetry { .. } => f.write_str("BlockchainConflictRetry"),
60 Self::Immutable => f.write_str("BlockchainCollectionImmutable"),
61 }
62 }
63}
64
65impl std::error::Error for BlockchainError {}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum VerifyReport {
72 Ok,
73 Inconsistent { block_height: u64, reason: String },
74}
75
76pub fn compute_block_hash(
92 prev_hash: &[u8; 32],
93 block_height: u64,
94 timestamp_ms: u64,
95 payload: &[u8],
96 signed: Option<&SignedFields>,
97) -> [u8; 32] {
98 let mut h = Sha256::new();
99 h.update(prev_hash);
100 h.update(&block_height.to_be_bytes());
101 h.update(×tamp_ms.to_be_bytes());
102 h.update(&(payload.len() as u64).to_be_bytes());
103 h.update(payload);
104 if let Some(s) = signed {
105 h.update(&s.signer_pubkey);
106 h.update(&(s.signature.len() as u64).to_be_bytes());
107 h.update(&s.signature);
108 }
109 h.finalize()
110}
111
112pub fn verify_chain(blocks: &[Block]) -> VerifyReport {
116 let mut expected_prev: [u8; 32] = GENESIS_PREV_HASH;
117 let mut expected_height: u64 = 0;
118 for block in blocks {
119 if block.block_height != expected_height {
120 return VerifyReport::Inconsistent {
121 block_height: block.block_height,
122 reason: format!(
123 "block_height mismatch: expected {expected_height}, got {}",
124 block.block_height
125 ),
126 };
127 }
128 if block.prev_hash != expected_prev {
129 return VerifyReport::Inconsistent {
130 block_height: block.block_height,
131 reason: "prev_hash does not link previous block".to_string(),
132 };
133 }
134 let recomputed = compute_block_hash(
135 &block.prev_hash,
136 block.block_height,
137 block.timestamp_ms,
138 &block.payload,
139 block.signed.as_ref(),
140 );
141 if recomputed != block.hash {
142 return VerifyReport::Inconsistent {
143 block_height: block.block_height,
144 reason: "stored hash does not match recomputed hash".to_string(),
145 };
146 }
147 expected_prev = block.hash;
148 expected_height = block.block_height.saturating_add(1);
149 }
150 VerifyReport::Ok
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn make_block(height: u64, prev: [u8; 32], payload: &[u8]) -> Block {
158 let ts = 1_700_000_000_000 + height;
159 let hash = compute_block_hash(&prev, height, ts, payload, None);
160 Block {
161 block_height: height,
162 prev_hash: prev,
163 timestamp_ms: ts,
164 payload: payload.to_vec(),
165 signed: None,
166 hash,
167 }
168 }
169
170 fn build_chain(n: u64) -> Vec<Block> {
171 let mut out = Vec::new();
172 let mut prev = GENESIS_PREV_HASH;
173 for i in 0..n {
174 let payload = format!("payload-{i}");
175 let b = make_block(i, prev, payload.as_bytes());
176 prev = b.hash;
177 out.push(b);
178 }
179 out
180 }
181
182 #[test]
183 fn genesis_prev_hash_is_zero() {
184 assert_eq!(GENESIS_PREV_HASH, [0u8; 32]);
185 }
186
187 #[test]
188 fn five_block_chain_verifies_ok() {
189 let chain = build_chain(5);
190 assert_eq!(verify_chain(&chain), VerifyReport::Ok);
191 assert_eq!(chain[0].block_height, 0);
192 assert_eq!(chain[0].prev_hash, GENESIS_PREV_HASH);
193 assert_eq!(chain[4].block_height, 4);
194 }
195
196 #[test]
197 fn corrupting_block_two_payload_is_reported() {
198 let mut chain = build_chain(5);
199 chain[2].payload = b"tampered".to_vec();
200 match verify_chain(&chain) {
201 VerifyReport::Inconsistent { block_height, .. } => {
202 assert_eq!(block_height, 2);
203 }
204 VerifyReport::Ok => panic!("tampered chain reported Ok"),
205 }
206 }
207
208 #[test]
209 fn corrupting_prev_hash_breaks_chain() {
210 let mut chain = build_chain(3);
211 chain[1].prev_hash = [0xAAu8; 32];
212 chain[1].hash = compute_block_hash(
215 &chain[1].prev_hash,
216 chain[1].block_height,
217 chain[1].timestamp_ms,
218 &chain[1].payload,
219 None,
220 );
221 match verify_chain(&chain) {
222 VerifyReport::Inconsistent {
223 block_height,
224 reason,
225 } => {
226 assert_eq!(block_height, 1);
227 assert!(reason.contains("prev_hash"));
228 }
229 VerifyReport::Ok => panic!("broken linkage reported Ok"),
230 }
231 }
232
233 #[test]
234 fn signed_field_inclusion_changes_hash() {
235 let prev = GENESIS_PREV_HASH;
236 let payload = b"x";
237 let unsigned = compute_block_hash(&prev, 0, 1, payload, None);
238 let signed = compute_block_hash(
239 &prev,
240 0,
241 1,
242 payload,
243 Some(&SignedFields {
244 signer_pubkey: [0x11; 32],
245 signature: vec![0x22; 64],
246 }),
247 );
248 assert_ne!(unsigned, signed);
249 }
250
251 #[test]
252 fn empty_payload_signed_vs_unsigned_disambiguates() {
253 let prev = GENESIS_PREV_HASH;
256 let signer = [0x55u8; 32];
257 let sig = vec![0x66u8; 8];
258 let signed = compute_block_hash(
259 &prev,
260 7,
261 42,
262 b"",
263 Some(&SignedFields {
264 signer_pubkey: signer,
265 signature: sig.clone(),
266 }),
267 );
268 let mut spoof_payload = Vec::new();
269 spoof_payload.extend_from_slice(&signer);
270 spoof_payload.extend_from_slice(&(sig.len() as u64).to_be_bytes());
271 spoof_payload.extend_from_slice(&sig);
272 let unsigned = compute_block_hash(&prev, 7, 42, &spoof_payload, None);
273 assert_ne!(signed, unsigned);
274 }
275
276 #[test]
277 fn conflict_retry_display() {
278 let err = BlockchainError::ConflictRetry {
279 expected: [1u8; 32],
280 got: [2u8; 32],
281 };
282 assert_eq!(err.to_string(), "BlockchainConflictRetry");
283 assert_eq!(
284 BlockchainError::Immutable.to_string(),
285 "BlockchainCollectionImmutable"
286 );
287 }
288}