diff --git a/lez/common/src/block.rs b/lez/common/src/block.rs index 6e956f9f..029bcfba 100644 --- a/lez/common/src/block.rs +++ b/lez/common/src/block.rs @@ -55,6 +55,21 @@ pub struct Block { pub bedrock_status: BedrockStatus, } +impl Block { + /// Recomputes the hash from this block's contents, for integrity verification + /// against the value stored in `header.hash`. + #[must_use] + pub fn recompute_hash(&self) -> BlockHash { + HashableBlockData { + block_id: self.header.block_id, + prev_block_hash: self.header.prev_block_hash, + timestamp: self.header.timestamp, + transactions: self.body.transactions.clone(), + } + .compute_hash() + } +} + impl Serialize for Block { fn serialize(&self, serializer: S) -> Result { crate::borsh_base64::serialize(self, serializer) @@ -76,11 +91,13 @@ pub struct HashableBlockData { } impl HashableBlockData { + /// Domain-separated hash of the block contents: `SHA256(PREFIX || borsh(self))`. + /// The single source of truth for both producing and verifying a block hash. #[must_use] - pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block { + pub fn compute_hash(&self) -> BlockHash { const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Block/\x00\x00\x00\x00\x00\x00\x00\x00"; - let data_bytes = borsh::to_vec(&self).unwrap(); + let data_bytes = borsh::to_vec(self).unwrap(); let mut bytes = Vec::with_capacity( PREFIX .len() @@ -89,8 +106,12 @@ impl HashableBlockData { ); bytes.extend_from_slice(PREFIX); bytes.extend_from_slice(&data_bytes); + OwnHasher::hash(&bytes) + } - let hash = OwnHasher::hash(&bytes); + #[must_use] + pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block { + let hash = self.compute_hash(); let signature = lee::Signature::new(signing_key, &hash.0); Block { header: BlockHeader { @@ -132,4 +153,33 @@ mod tests { let block_from_bytes = borsh::from_slice::(&bytes).unwrap(); assert_eq!(hashable, block_from_bytes); } + + #[test] + fn recompute_hash_matches_header_for_well_formed_block() { + let key = lee::PrivateKey::try_new([7_u8; 32]).expect("valid key"); + let block = HashableBlockData { + block_id: 5, + prev_block_hash: HashType([9_u8; 32]), + timestamp: 42, + transactions: vec![test_utils::produce_dummy_empty_transaction()], + } + .into_pending_block(&key); + assert_eq!(block.recompute_hash(), block.header.hash); + } + + #[test] + fn recompute_hash_detects_tampering() { + let key = lee::PrivateKey::try_new([7_u8; 32]).expect("valid key"); + let block = HashableBlockData { + block_id: 5, + prev_block_hash: HashType([9_u8; 32]), + timestamp: 42, + transactions: vec![test_utils::produce_dummy_empty_transaction()], + } + .into_pending_block(&key); + + let mut tampered = block.clone(); + tampered.header.timestamp = 99; // header changed; stale hash no longer matches + assert_ne!(tampered.recompute_hash(), tampered.header.hash); + } }