feat(common): add block hash recomputation helper

test:  `RISC0_DEV_MODE=1 RISC0_SKIP_BUILD=1 cargo test -p common --lib block::tests`
This commit is contained in:
erhant 2026-06-26 13:49:56 +03:00
parent b728074225
commit f0dec9dd72

View File

@ -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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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::<HashableBlockData>(&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);
}
}