use crate::crypto::Sha256;
pub const GENESIS_PREV_HASH: [u8; 32] = [0u8; 32];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedFields {
pub signer_pubkey: [u8; 32],
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Block {
pub block_height: u64,
pub prev_hash: [u8; 32],
pub timestamp_ms: u64,
pub payload: Vec<u8>,
pub signed: Option<SignedFields>,
pub hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainTip {
pub block_height: u64,
pub hash: [u8; 32],
pub timestamp_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockchainError {
ConflictRetry {
expected: [u8; 32],
got: [u8; 32],
},
Immutable,
}
impl std::fmt::Display for BlockchainError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConflictRetry { .. } => f.write_str("BlockchainConflictRetry"),
Self::Immutable => f.write_str("BlockchainCollectionImmutable"),
}
}
}
impl std::error::Error for BlockchainError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyReport {
Ok,
Inconsistent { block_height: u64, reason: String },
}
pub fn compute_block_hash(
prev_hash: &[u8; 32],
block_height: u64,
timestamp_ms: u64,
payload: &[u8],
signed: Option<&SignedFields>,
) -> [u8; 32] {
let mut h = Sha256::new();
h.update(prev_hash);
h.update(&block_height.to_be_bytes());
h.update(×tamp_ms.to_be_bytes());
h.update(&(payload.len() as u64).to_be_bytes());
h.update(payload);
if let Some(s) = signed {
h.update(&s.signer_pubkey);
h.update(&(s.signature.len() as u64).to_be_bytes());
h.update(&s.signature);
}
h.finalize()
}
pub fn verify_chain(blocks: &[Block]) -> VerifyReport {
let mut expected_prev: [u8; 32] = GENESIS_PREV_HASH;
let mut expected_height: u64 = 0;
for block in blocks {
if block.block_height != expected_height {
return VerifyReport::Inconsistent {
block_height: block.block_height,
reason: format!(
"block_height mismatch: expected {expected_height}, got {}",
block.block_height
),
};
}
if block.prev_hash != expected_prev {
return VerifyReport::Inconsistent {
block_height: block.block_height,
reason: "prev_hash does not link previous block".to_string(),
};
}
let recomputed = compute_block_hash(
&block.prev_hash,
block.block_height,
block.timestamp_ms,
&block.payload,
block.signed.as_ref(),
);
if recomputed != block.hash {
return VerifyReport::Inconsistent {
block_height: block.block_height,
reason: "stored hash does not match recomputed hash".to_string(),
};
}
expected_prev = block.hash;
expected_height = block.block_height.saturating_add(1);
}
VerifyReport::Ok
}
#[cfg(test)]
mod tests {
use super::*;
fn make_block(height: u64, prev: [u8; 32], payload: &[u8]) -> Block {
let ts = 1_700_000_000_000 + height;
let hash = compute_block_hash(&prev, height, ts, payload, None);
Block {
block_height: height,
prev_hash: prev,
timestamp_ms: ts,
payload: payload.to_vec(),
signed: None,
hash,
}
}
fn build_chain(n: u64) -> Vec<Block> {
let mut out = Vec::new();
let mut prev = GENESIS_PREV_HASH;
for i in 0..n {
let payload = format!("payload-{i}");
let b = make_block(i, prev, payload.as_bytes());
prev = b.hash;
out.push(b);
}
out
}
#[test]
fn genesis_prev_hash_is_zero() {
assert_eq!(GENESIS_PREV_HASH, [0u8; 32]);
}
#[test]
fn five_block_chain_verifies_ok() {
let chain = build_chain(5);
assert_eq!(verify_chain(&chain), VerifyReport::Ok);
assert_eq!(chain[0].block_height, 0);
assert_eq!(chain[0].prev_hash, GENESIS_PREV_HASH);
assert_eq!(chain[4].block_height, 4);
}
#[test]
fn corrupting_block_two_payload_is_reported() {
let mut chain = build_chain(5);
chain[2].payload = b"tampered".to_vec();
match verify_chain(&chain) {
VerifyReport::Inconsistent { block_height, .. } => {
assert_eq!(block_height, 2);
}
VerifyReport::Ok => panic!("tampered chain reported Ok"),
}
}
#[test]
fn corrupting_prev_hash_breaks_chain() {
let mut chain = build_chain(3);
chain[1].prev_hash = [0xAAu8; 32];
chain[1].hash = compute_block_hash(
&chain[1].prev_hash,
chain[1].block_height,
chain[1].timestamp_ms,
&chain[1].payload,
None,
);
match verify_chain(&chain) {
VerifyReport::Inconsistent { block_height, reason } => {
assert_eq!(block_height, 1);
assert!(reason.contains("prev_hash"));
}
VerifyReport::Ok => panic!("broken linkage reported Ok"),
}
}
#[test]
fn signed_field_inclusion_changes_hash() {
let prev = GENESIS_PREV_HASH;
let payload = b"x";
let unsigned = compute_block_hash(&prev, 0, 1, payload, None);
let signed = compute_block_hash(
&prev,
0,
1,
payload,
Some(&SignedFields {
signer_pubkey: [0x11; 32],
signature: vec![0x22; 64],
}),
);
assert_ne!(unsigned, signed);
}
#[test]
fn empty_payload_signed_vs_unsigned_disambiguates() {
let prev = GENESIS_PREV_HASH;
let signer = [0x55u8; 32];
let sig = vec![0x66u8; 8];
let signed = compute_block_hash(
&prev,
7,
42,
b"",
Some(&SignedFields {
signer_pubkey: signer,
signature: sig.clone(),
}),
);
let mut spoof_payload = Vec::new();
spoof_payload.extend_from_slice(&signer);
spoof_payload.extend_from_slice(&(sig.len() as u64).to_be_bytes());
spoof_payload.extend_from_slice(&sig);
let unsigned = compute_block_hash(&prev, 7, 42, &spoof_payload, None);
assert_ne!(signed, unsigned);
}
#[test]
fn conflict_retry_display() {
let err = BlockchainError::ConflictRetry {
expected: [1u8; 32],
got: [2u8; 32],
};
assert_eq!(err.to_string(), "BlockchainConflictRetry");
assert_eq!(
BlockchainError::Immutable.to_string(),
"BlockchainCollectionImmutable"
);
}
}