1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct BlockLogHeader {
11 pub batch_seq: u64,
13 pub prev_block_hash: [u8; 32],
15 pub commands_hash: [u8; 32],
17 pub batch_root: [u8; 32],
19 pub command_count: u64,
21 pub first_seq: u64,
23 pub timestamp: u64,
25 pub signer: [u8; 20],
27 pub signature: Vec<u8>,
29}
30
31impl BlockLogHeader {
32 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 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 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
96pub 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}