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 {
51 expected: [u8; 32],
52 got: [u8; 32],
53 },
54 Immutable,
57}
58
59impl std::fmt::Display for BlockchainError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::ConflictRetry { .. } => f.write_str("BlockchainConflictRetry"),
63 Self::Immutable => f.write_str("BlockchainCollectionImmutable"),
64 }
65 }
66}
67
68impl std::error::Error for BlockchainError {}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum VerifyReport {
75 Ok,
76 Inconsistent { block_height: u64, reason: String },
77}
78
79pub fn compute_block_hash(
95 prev_hash: &[u8; 32],
96 block_height: u64,
97 timestamp_ms: u64,
98 payload: &[u8],
99 signed: Option<&SignedFields>,
100) -> [u8; 32] {
101 let mut h = Sha256::new();
102 h.update(prev_hash);
103 h.update(&block_height.to_be_bytes());
104 h.update(×tamp_ms.to_be_bytes());
105 h.update(&(payload.len() as u64).to_be_bytes());
106 h.update(payload);
107 if let Some(s) = signed {
108 h.update(&s.signer_pubkey);
109 h.update(&(s.signature.len() as u64).to_be_bytes());
110 h.update(&s.signature);
111 }
112 h.finalize()
113}
114
115pub fn verify_chain(blocks: &[Block]) -> VerifyReport {
119 let mut expected_prev: [u8; 32] = GENESIS_PREV_HASH;
120 let mut expected_height: u64 = 0;
121 for block in blocks {
122 if block.block_height != expected_height {
123 return VerifyReport::Inconsistent {
124 block_height: block.block_height,
125 reason: format!(
126 "block_height mismatch: expected {expected_height}, got {}",
127 block.block_height
128 ),
129 };
130 }
131 if block.prev_hash != expected_prev {
132 return VerifyReport::Inconsistent {
133 block_height: block.block_height,
134 reason: "prev_hash does not link previous block".to_string(),
135 };
136 }
137 let recomputed = compute_block_hash(
138 &block.prev_hash,
139 block.block_height,
140 block.timestamp_ms,
141 &block.payload,
142 block.signed.as_ref(),
143 );
144 if recomputed != block.hash {
145 return VerifyReport::Inconsistent {
146 block_height: block.block_height,
147 reason: "stored hash does not match recomputed hash".to_string(),
148 };
149 }
150 expected_prev = block.hash;
151 expected_height = block.block_height.saturating_add(1);
152 }
153 VerifyReport::Ok
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn make_block(height: u64, prev: [u8; 32], payload: &[u8]) -> Block {
161 let ts = 1_700_000_000_000 + height;
162 let hash = compute_block_hash(&prev, height, ts, payload, None);
163 Block {
164 block_height: height,
165 prev_hash: prev,
166 timestamp_ms: ts,
167 payload: payload.to_vec(),
168 signed: None,
169 hash,
170 }
171 }
172
173 fn build_chain(n: u64) -> Vec<Block> {
174 let mut out = Vec::new();
175 let mut prev = GENESIS_PREV_HASH;
176 for i in 0..n {
177 let payload = format!("payload-{i}");
178 let b = make_block(i, prev, payload.as_bytes());
179 prev = b.hash;
180 out.push(b);
181 }
182 out
183 }
184
185 #[test]
186 fn genesis_prev_hash_is_zero() {
187 assert_eq!(GENESIS_PREV_HASH, [0u8; 32]);
188 }
189
190 #[test]
191 fn five_block_chain_verifies_ok() {
192 let chain = build_chain(5);
193 assert_eq!(verify_chain(&chain), VerifyReport::Ok);
194 assert_eq!(chain[0].block_height, 0);
195 assert_eq!(chain[0].prev_hash, GENESIS_PREV_HASH);
196 assert_eq!(chain[4].block_height, 4);
197 }
198
199 #[test]
200 fn corrupting_block_two_payload_is_reported() {
201 let mut chain = build_chain(5);
202 chain[2].payload = b"tampered".to_vec();
203 match verify_chain(&chain) {
204 VerifyReport::Inconsistent { block_height, .. } => {
205 assert_eq!(block_height, 2);
206 }
207 VerifyReport::Ok => panic!("tampered chain reported Ok"),
208 }
209 }
210
211 #[test]
212 fn corrupting_prev_hash_breaks_chain() {
213 let mut chain = build_chain(3);
214 chain[1].prev_hash = [0xAAu8; 32];
215 chain[1].hash = compute_block_hash(
218 &chain[1].prev_hash,
219 chain[1].block_height,
220 chain[1].timestamp_ms,
221 &chain[1].payload,
222 None,
223 );
224 match verify_chain(&chain) {
225 VerifyReport::Inconsistent { block_height, reason } => {
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}