Skip to main content

hypercall_blocks/
header.rs

1use serde::{Deserialize, Serialize};
2
3/// Serializable signed block header stored in the WAL block log.
4///
5/// The header commits to an ordered batch of engine commands through
6/// `commands_hash`, and to the previous block through `prev_block_hash`.
7/// The signature is stored next to the signed fields, but is intentionally
8/// excluded from `block_hash()`.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct BlockLogHeader {
11    /// Monotonic block number, starting at zero.
12    pub batch_seq: u64,
13    /// Hash of the previous block header. Zero for the genesis block.
14    pub prev_block_hash: [u8; 32],
15    /// `keccak256(cmd1_identity_hash || cmd2_identity_hash || ...)`.
16    pub commands_hash: [u8; 32],
17    /// Composite commitment root for state, risk, command, obligation, and intent roots.
18    pub batch_root: [u8; 32],
19    /// Number of commands committed by this block.
20    pub command_count: u64,
21    /// Global command sequence number of the first command in this block.
22    pub first_seq: u64,
23    /// Timestamp in milliseconds when the block was finalized.
24    pub timestamp: u64,
25    /// Public address of the signer encoded as raw EVM address bytes.
26    pub signer: [u8; 20],
27    /// Signature bytes over `block_hash()`. Empty means unsigned.
28    pub signature: Vec<u8>,
29}
30
31impl BlockLogHeader {
32    /// Build an unsigned block-log header from ordered command identity hashes.
33    ///
34    /// This is intentionally pure. Callers provide sequencing, timestamp,
35    /// signer identity, and batch root. The function derives only the command
36    /// commitment and command count.
37    pub fn unsigned_from_identity_hashes(
38        batch_seq: u64,
39        prev_block_hash: [u8; 32],
40        identity_hashes: &[[u8; 32]],
41        batch_root: [u8; 32],
42        first_seq: u64,
43        timestamp: u64,
44        signer: [u8; 20],
45    ) -> Self {
46        Self {
47            batch_seq,
48            prev_block_hash,
49            commands_hash: compute_commands_hash(identity_hashes),
50            batch_root,
51            command_count: identity_hashes.len() as u64,
52            first_seq,
53            timestamp,
54            signer,
55            signature: Vec::new(),
56        }
57    }
58
59    /// Compute the block hash over the signed fields. The signature is excluded.
60    pub fn block_hash(&self) -> [u8; 32] {
61        use sha3::{Digest, Keccak256};
62
63        let mut h = Keccak256::new();
64        h.update(self.batch_seq.to_be_bytes());
65        h.update(self.prev_block_hash);
66        h.update(self.commands_hash);
67        h.update(self.batch_root);
68        h.update(self.command_count.to_be_bytes());
69        h.update(self.first_seq.to_be_bytes());
70        h.update(self.timestamp.to_be_bytes());
71        h.update(self.signer);
72        h.finalize().into()
73    }
74
75    /// Compute the Solidity `ValidatorRsm.blockHash(metadata)` value for this header.
76    ///
77    /// This is intentionally distinct from `block_hash()`. The WAL block hash signs
78    /// local log metadata, including timestamp and signer, while ValidatorRsm commits
79    /// to the on-chain directive hash instead.
80    pub fn validator_rsm_metadata_hash(&self, directive_hash: [u8; 32]) -> [u8; 32] {
81        use sha3::{Digest, Keccak256};
82
83        let mut encoded = Vec::with_capacity(32 * 7);
84        push_abi_u64_word(&mut encoded, self.batch_seq);
85        encoded.extend_from_slice(&self.prev_block_hash);
86        encoded.extend_from_slice(&self.commands_hash);
87        encoded.extend_from_slice(&self.batch_root);
88        push_abi_u64_word(&mut encoded, self.command_count);
89        push_abi_u64_word(&mut encoded, self.first_seq);
90        encoded.extend_from_slice(&directive_hash);
91
92        Keccak256::digest(encoded).into()
93    }
94}
95
96/// Compute `keccak256(cmd1_identity_hash || cmd2_identity_hash || ...)`.
97pub fn compute_commands_hash(identity_hashes: &[[u8; 32]]) -> [u8; 32] {
98    use sha3::{Digest, Keccak256};
99
100    let mut h = Keccak256::new();
101    for hash in identity_hashes {
102        h.update(hash);
103    }
104    h.finalize().into()
105}
106
107fn push_abi_u64_word(out: &mut Vec<u8>, value: u64) {
108    out.extend_from_slice(&[0u8; 24]);
109    out.extend_from_slice(&value.to_be_bytes());
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn baseline_header() -> BlockLogHeader {
117        BlockLogHeader {
118            batch_seq: 1,
119            prev_block_hash: [2; 32],
120            commands_hash: [3; 32],
121            batch_root: [4; 32],
122            command_count: 5,
123            first_seq: 6,
124            timestamp: 1_700_000_000,
125            signer: [7; 20],
126            signature: vec![8; 65],
127        }
128    }
129
130    #[test]
131    fn unsigned_header_derives_commands_hash_and_count() {
132        let identities = [[1u8; 32], [2u8; 32], [3u8; 32]];
133        let header = BlockLogHeader::unsigned_from_identity_hashes(
134            7,
135            [9; 32],
136            &identities,
137            [10; 32],
138            42,
139            1_700_000_000,
140            [11; 20],
141        );
142
143        assert_eq!(header.batch_seq, 7);
144        assert_eq!(header.prev_block_hash, [9; 32]);
145        assert_eq!(header.commands_hash, compute_commands_hash(&identities));
146        assert_eq!(header.batch_root, [10; 32]);
147        assert_eq!(header.command_count, 3);
148        assert_eq!(header.first_seq, 42);
149        assert_eq!(header.timestamp, 1_700_000_000);
150        assert_eq!(header.signer, [11; 20]);
151        assert!(header.signature.is_empty());
152    }
153
154    #[test]
155    fn command_hash_order_matters() {
156        let a = [1u8; 32];
157        let b = [2u8; 32];
158
159        assert_ne!(
160            compute_commands_hash(&[a, b]),
161            compute_commands_hash(&[b, a])
162        );
163    }
164
165    #[test]
166    fn command_hash_empty_batch_is_still_domain_data() {
167        let empty = compute_commands_hash(&[]);
168
169        assert_ne!(empty, [0u8; 32]);
170        assert_eq!(empty, compute_commands_hash(&[]));
171    }
172
173    #[test]
174    fn command_hash_single_batch_is_deterministic_and_not_identity() {
175        let identity = [0xabu8; 32];
176        let hash = compute_commands_hash(&[identity]);
177
178        assert_eq!(hash, compute_commands_hash(&[identity]));
179        assert_ne!(hash, identity);
180    }
181
182    #[test]
183    fn command_hash_large_batch_is_deterministic() {
184        let hashes: Vec<[u8; 32]> = (0..10_000u32)
185            .map(|i| {
186                let mut hash = [0u8; 32];
187                hash[..4].copy_from_slice(&i.to_be_bytes());
188                hash
189            })
190            .collect();
191
192        assert_eq!(
193            compute_commands_hash(&hashes),
194            compute_commands_hash(&hashes)
195        );
196    }
197
198    #[test]
199    fn block_hash_is_deterministic() {
200        let header = baseline_header();
201
202        assert_eq!(header.block_hash(), header.block_hash());
203    }
204
205    #[test]
206    fn block_hash_excludes_signature() {
207        let mut header = baseline_header();
208        let hash = header.block_hash();
209
210        header.signature = vec![0xff; 65];
211
212        assert_eq!(hash, header.block_hash());
213    }
214
215    #[test]
216    fn block_hash_changes_when_signed_field_changes() {
217        let baseline = baseline_header();
218        let baseline_hash = baseline.block_hash();
219
220        let mut changed = baseline.clone();
221        changed.batch_seq += 1;
222        assert_ne!(baseline_hash, changed.block_hash());
223
224        let mut changed = baseline.clone();
225        changed.prev_block_hash = [0x10; 32];
226        assert_ne!(baseline_hash, changed.block_hash());
227
228        let mut changed = baseline.clone();
229        changed.commands_hash = [0x20; 32];
230        assert_ne!(baseline_hash, changed.block_hash());
231
232        let mut changed = baseline.clone();
233        changed.batch_root = [0x30; 32];
234        assert_ne!(baseline_hash, changed.block_hash());
235
236        let mut changed = baseline.clone();
237        changed.command_count += 1;
238        assert_ne!(baseline_hash, changed.block_hash());
239
240        let mut changed = baseline.clone();
241        changed.first_seq += 1;
242        assert_ne!(baseline_hash, changed.block_hash());
243
244        let mut changed = baseline.clone();
245        changed.timestamp += 1;
246        assert_ne!(baseline_hash, changed.block_hash());
247
248        let mut changed = baseline;
249        changed.signer = [0x40; 20];
250        assert_ne!(baseline_hash, changed.block_hash());
251    }
252
253    #[test]
254    fn validator_rsm_metadata_hash_matches_solidity_vector() {
255        let header = BlockLogHeader {
256            batch_seq: 7,
257            prev_block_hash: [0xaa; 32],
258            commands_hash: [0xbb; 32],
259            batch_root: [0xcc; 32],
260            command_count: 3,
261            first_seq: 42,
262            timestamp: 1_700_000_000,
263            signer: [0x11; 20],
264            signature: vec![0x22; 65],
265        };
266        let directive_hash = [
267            0x23, 0xc3, 0x04, 0xad, 0xf8, 0xd5, 0x7c, 0xba, 0xc7, 0x83, 0x4a, 0x6c, 0xe5, 0xdf,
268            0x58, 0xc8, 0xd3, 0x20, 0x6e, 0xbb, 0x0e, 0xb1, 0xd9, 0x2e, 0x31, 0xe5, 0xaf, 0x71,
269            0x2d, 0xff, 0x69, 0x98,
270        ];
271
272        assert_eq!(
273            header.validator_rsm_metadata_hash(directive_hash),
274            [
275                0x72, 0x69, 0x24, 0xa7, 0x66, 0x2f, 0xdd, 0xa1, 0xb8, 0xc7, 0x03, 0x4d, 0xac, 0x40,
276                0x30, 0x34, 0x8b, 0xe8, 0x2c, 0x23, 0xd0, 0x92, 0xfe, 0xac, 0xb5, 0x8e, 0xa1, 0x73,
277                0x08, 0x58, 0x3d, 0x6d,
278            ]
279        );
280    }
281
282    #[test]
283    fn validator_rsm_metadata_hash_uses_onchain_metadata_only() {
284        let header = baseline_header();
285        let directive_hash = [0x55; 32];
286        let baseline_hash = header.validator_rsm_metadata_hash(directive_hash);
287
288        let mut changed = header.clone();
289        changed.timestamp += 1;
290        assert_eq!(
291            baseline_hash,
292            changed.validator_rsm_metadata_hash(directive_hash)
293        );
294
295        let mut changed = header.clone();
296        changed.signer = [0x66; 20];
297        assert_eq!(
298            baseline_hash,
299            changed.validator_rsm_metadata_hash(directive_hash)
300        );
301
302        let mut changed = header.clone();
303        changed.signature = vec![0x77; 65];
304        assert_eq!(
305            baseline_hash,
306            changed.validator_rsm_metadata_hash(directive_hash)
307        );
308
309        let mut changed = header.clone();
310        changed.command_count += 1;
311        assert_ne!(
312            baseline_hash,
313            changed.validator_rsm_metadata_hash(directive_hash)
314        );
315
316        assert_ne!(
317            baseline_hash,
318            header.validator_rsm_metadata_hash([0x88; 32])
319        );
320    }
321
322    #[test]
323    fn block_chain_links_by_previous_hash() {
324        let block0 = BlockLogHeader::unsigned_from_identity_hashes(
325            0,
326            [0; 32],
327            &[[1; 32]],
328            [0; 32],
329            0,
330            1_700_000_000,
331            [9; 20],
332        );
333        let block1 = BlockLogHeader::unsigned_from_identity_hashes(
334            1,
335            block0.block_hash(),
336            &[[2; 32]],
337            [0; 32],
338            1,
339            1_700_000_001,
340            [9; 20],
341        );
342        let block2 = BlockLogHeader::unsigned_from_identity_hashes(
343            2,
344            block1.block_hash(),
345            &[[3; 32]],
346            [0; 32],
347            2,
348            1_700_000_002,
349            [9; 20],
350        );
351
352        assert_eq!(block0.prev_block_hash, [0; 32]);
353        assert_eq!(block1.prev_block_hash, block0.block_hash());
354        assert_eq!(block2.prev_block_hash, block1.block_hash());
355        assert_ne!(block0.block_hash(), block1.block_hash());
356        assert_ne!(block1.block_hash(), block2.block_hash());
357    }
358}