1use alloy::{
2 primitives::{keccak256, Address, FixedBytes, U256},
3 sol,
4 sol_types::{Eip712Domain, SolStruct, SolValue},
5};
6
7use crate::directives::{HYPERCALL_MAINNET_CHAIN_ID, HYPERCALL_TESTNET_CHAIN_ID};
8
9pub const VALIDATOR_RSM_DOMAIN_NAME: &str = "HypercallValidatorRsm";
11pub const VALIDATOR_RSM_DOMAIN_VERSION: &str = "1";
13pub const VALIDATOR_RSM_TYPE_VERSION: u16 = 1;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ValidatorRsmError {
19 UnsupportedChainId(u64),
21 ZeroVerifyingContract,
23}
24
25impl std::fmt::Display for ValidatorRsmError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Self::UnsupportedChainId(chain_id) => {
29 write!(f, "Unsupported RSM attestation chain id: {}", chain_id)
30 }
31 Self::ZeroVerifyingContract => {
32 write!(f, "RSM attestation verifying contract must not be zero")
33 }
34 }
35 }
36}
37
38impl std::error::Error for ValidatorRsmError {}
39
40sol! {
41 #[derive(Debug, PartialEq, Eq)]
43 struct ValidatorRsmBatchMetadata {
44 uint64 batchSeq;
45 bytes32 prevBlockHash;
46 bytes32 commandsHash;
47 bytes32 batchRoot;
48 uint64 commandCount;
49 uint64 firstSeq;
50 bytes32 directiveHash;
51 }
52}
53
54pub fn rsm_attestation_domain(
56 chain_id: u64,
57 verifying_contract: Address,
58) -> Result<Eip712Domain, ValidatorRsmError> {
59 if chain_id != HYPERCALL_TESTNET_CHAIN_ID && chain_id != HYPERCALL_MAINNET_CHAIN_ID {
60 return Err(ValidatorRsmError::UnsupportedChainId(chain_id));
61 }
62 if verifying_contract == Address::ZERO {
63 return Err(ValidatorRsmError::ZeroVerifyingContract);
64 }
65
66 Ok(Eip712Domain {
67 name: Some(VALIDATOR_RSM_DOMAIN_NAME.into()),
68 version: Some(VALIDATOR_RSM_DOMAIN_VERSION.into()),
69 chain_id: Some(U256::from(chain_id)),
70 verifying_contract: Some(verifying_contract),
71 salt: None,
72 })
73}
74
75pub fn rsm_batch_metadata_struct_hash(metadata: &ValidatorRsmBatchMetadata) -> FixedBytes<32> {
77 metadata.eip712_hash_struct()
78}
79
80pub fn rsm_batch_metadata_block_hash(metadata: &ValidatorRsmBatchMetadata) -> FixedBytes<32> {
82 keccak256(metadata.abi_encode())
83}
84
85pub fn rsm_batch_metadata_from_block_fields(
91 batch_seq: u64,
92 prev_block_hash: [u8; 32],
93 commands_hash: [u8; 32],
94 batch_root: [u8; 32],
95 command_count: u64,
96 first_seq: u64,
97 directive: &[u8],
98) -> ValidatorRsmBatchMetadata {
99 ValidatorRsmBatchMetadata {
100 batchSeq: batch_seq,
101 prevBlockHash: FixedBytes::from(prev_block_hash),
102 commandsHash: FixedBytes::from(commands_hash),
103 batchRoot: FixedBytes::from(batch_root),
104 commandCount: command_count,
105 firstSeq: first_seq,
106 directiveHash: rsm_directive_hash(directive),
107 }
108}
109
110pub fn verify_rsm_batch_metadata_block_hash(
112 metadata: &ValidatorRsmBatchMetadata,
113 expected_block_hash: FixedBytes<32>,
114) -> bool {
115 rsm_batch_metadata_block_hash(metadata) == expected_block_hash
116}
117
118pub fn rsm_directive_hash(directive: &[u8]) -> FixedBytes<32> {
120 keccak256(directive)
121}
122
123pub fn rsm_batch_metadata_signing_hash(
125 metadata: &ValidatorRsmBatchMetadata,
126 chain_id: u64,
127 verifying_contract: Address,
128) -> Result<FixedBytes<32>, ValidatorRsmError> {
129 let domain = rsm_attestation_domain(chain_id, verifying_contract)?;
130 Ok(metadata.eip712_signing_hash(&domain))
131}
132
133pub fn verify_rsm_batch_metadata_signing_hash(
135 metadata: &ValidatorRsmBatchMetadata,
136 chain_id: u64,
137 verifying_contract: Address,
138 expected_signing_hash: FixedBytes<32>,
139) -> Result<bool, ValidatorRsmError> {
140 Ok(
141 rsm_batch_metadata_signing_hash(metadata, chain_id, verifying_contract)?
142 == expected_signing_hash,
143 )
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use alloy::primitives::{address, b256};
150
151 fn vector_contract() -> Address {
152 address!("0x1111111111111111111111111111111111111111")
153 }
154
155 fn vector_metadata() -> ValidatorRsmBatchMetadata {
156 ValidatorRsmBatchMetadata {
157 batchSeq: 7,
158 prevBlockHash: b256!(
159 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
160 ),
161 commandsHash: b256!(
162 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
163 ),
164 batchRoot: b256!("0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
165 commandCount: 3,
166 firstSeq: 42,
167 directiveHash: b256!(
168 "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
169 ),
170 }
171 }
172
173 #[test]
174 fn rsm_batch_metadata_type_hash_matches_vector() {
175 assert_eq!(
176 format!(
177 "{:#x}",
178 keccak256(ValidatorRsmBatchMetadata::eip712_encode_type().as_bytes())
179 ),
180 "0x5eab7bc9925825c6321b3737a205ae360c425df72b286c62fcb6d6461889fdec"
181 );
182 }
183
184 #[test]
185 fn rsm_attestation_domain_separator_matches_vector() {
186 let domain = rsm_attestation_domain(HYPERCALL_TESTNET_CHAIN_ID, vector_contract()).unwrap();
187 assert_eq!(
188 format!("{:#x}", domain.hash_struct()),
189 "0x1064700185b52737a64ef42071090ce253524f99cc4a31103c862c3093183917"
190 );
191 }
192
193 #[test]
194 fn rsm_batch_metadata_struct_hash_matches_vector() {
195 assert_eq!(
196 format!("{:#x}", rsm_batch_metadata_struct_hash(&vector_metadata())),
197 "0x467441eb6abda1b71c63dea883a5c908be089aa182cb2b92c8d92ecd8383a65a"
198 );
199 }
200
201 #[test]
202 fn rsm_batch_metadata_signing_hash_matches_vector() {
203 assert_eq!(
204 format!(
205 "0x{}",
206 hex::encode(
207 rsm_batch_metadata_signing_hash(
208 &vector_metadata(),
209 HYPERCALL_TESTNET_CHAIN_ID,
210 vector_contract()
211 )
212 .unwrap()
213 )
214 ),
215 "0x29d7c6368cb595fb0bab304268eeba68545f00fa973119f5713a4093f9d29298"
216 );
217 }
218
219 #[test]
220 fn rsm_batch_metadata_block_hash_matches_solidity_vector() {
221 assert_eq!(
222 format!("{:#x}", rsm_batch_metadata_block_hash(&vector_metadata())),
223 "0xc321d2803249bd565800c1dd0160762abe459f87efe066c1e07034100e8f7383"
224 );
225 }
226
227 #[test]
228 fn rsm_directive_hash_uses_exact_encoded_bytes() {
229 let directive = hex::decode("1234abcd00010203040506070809").unwrap();
230 let mut changed = directive.clone();
231 changed.push(0);
232
233 assert_eq!(
234 format!("{:#x}", rsm_directive_hash(&directive)),
235 "0x23c304adf8d57cbac7834a6ce5df58c8d3206ebb0eb1d92e31e5af712dff6998"
236 );
237 assert_ne!(rsm_directive_hash(&directive), rsm_directive_hash(&changed));
238 }
239
240 #[test]
241 fn rsm_batch_metadata_from_block_fields_matches_solidity_hash_path() {
242 let directive = hex::decode("1234abcd00010203040506070809").unwrap();
243 let metadata = rsm_batch_metadata_from_block_fields(
244 7, [0xaa; 32], [0xbb; 32], [0xcc; 32], 3, 42, &directive,
245 );
246
247 assert_eq!(metadata.batchSeq, 7);
248 assert_eq!(metadata.prevBlockHash, FixedBytes::from([0xaa; 32]));
249 assert_eq!(metadata.commandsHash, FixedBytes::from([0xbb; 32]));
250 assert_eq!(metadata.batchRoot, FixedBytes::from([0xcc; 32]));
251 assert_eq!(metadata.commandCount, 3);
252 assert_eq!(metadata.firstSeq, 42);
253 assert_eq!(metadata.directiveHash, rsm_directive_hash(&directive));
254 assert_eq!(
255 format!("{:#x}", metadata.directiveHash),
256 "0x23c304adf8d57cbac7834a6ce5df58c8d3206ebb0eb1d92e31e5af712dff6998"
257 );
258 assert_eq!(
259 format!("{:#x}", rsm_batch_metadata_block_hash(&metadata)),
260 "0x726924a7662fdda1b8c7034dac4030348be82c23d092feacb58ea17308583d6d"
261 );
262 assert_eq!(
263 format!(
264 "{:#x}",
265 rsm_batch_metadata_signing_hash(
266 &metadata,
267 HYPERCALL_TESTNET_CHAIN_ID,
268 vector_contract()
269 )
270 .unwrap()
271 ),
272 "0xd9fd769dd0090b48ea792e9682d674f8e858cb17ddcfcd5673de81b942b64bfa"
273 );
274 }
275
276 #[test]
277 fn rsm_batch_metadata_verify_helpers_reject_tampering() {
278 let metadata = vector_metadata();
279 let block_hash = rsm_batch_metadata_block_hash(&metadata);
280 let signing_hash = rsm_batch_metadata_signing_hash(
281 &metadata,
282 HYPERCALL_TESTNET_CHAIN_ID,
283 vector_contract(),
284 )
285 .unwrap();
286
287 assert!(verify_rsm_batch_metadata_block_hash(&metadata, block_hash));
288 assert!(verify_rsm_batch_metadata_signing_hash(
289 &metadata,
290 HYPERCALL_TESTNET_CHAIN_ID,
291 vector_contract(),
292 signing_hash
293 )
294 .unwrap());
295
296 let mut tampered = metadata;
297 tampered.commandCount += 1;
298 assert!(!verify_rsm_batch_metadata_block_hash(&tampered, block_hash));
299 assert!(!verify_rsm_batch_metadata_signing_hash(
300 &tampered,
301 HYPERCALL_TESTNET_CHAIN_ID,
302 vector_contract(),
303 signing_hash
304 )
305 .unwrap());
306 }
307
308 #[test]
309 fn rsm_batch_metadata_field_mutations_change_signing_hash() {
310 let base = vector_metadata();
311 let base_hash =
312 rsm_batch_metadata_signing_hash(&base, HYPERCALL_TESTNET_CHAIN_ID, vector_contract())
313 .unwrap();
314
315 let mut cases = Vec::new();
316
317 let mut changed = base.clone();
318 changed.batchSeq += 1;
319 cases.push(changed);
320
321 let mut changed = base.clone();
322 changed.prevBlockHash = FixedBytes::from([0x11; 32]);
323 cases.push(changed);
324
325 let mut changed = base.clone();
326 changed.commandsHash = FixedBytes::from([0x22; 32]);
327 cases.push(changed);
328
329 let mut changed = base.clone();
330 changed.batchRoot = FixedBytes::from([0x33; 32]);
331 cases.push(changed);
332
333 let mut changed = base.clone();
334 changed.commandCount += 1;
335 cases.push(changed);
336
337 let mut changed = base.clone();
338 changed.firstSeq += 1;
339 cases.push(changed);
340
341 let mut changed = base;
342 changed.directiveHash = FixedBytes::from([0x44; 32]);
343 cases.push(changed);
344
345 for changed in cases {
346 let changed_hash = rsm_batch_metadata_signing_hash(
347 &changed,
348 HYPERCALL_TESTNET_CHAIN_ID,
349 vector_contract(),
350 )
351 .unwrap();
352 assert_ne!(base_hash, changed_hash);
353 }
354 }
355
356 #[test]
357 fn rsm_batch_metadata_field_mutations_change_block_hash() {
358 let base = vector_metadata();
359 let base_hash = rsm_batch_metadata_block_hash(&base);
360 let mut changed = base.clone();
361 changed.batchSeq += 1;
362 assert_ne!(base_hash, rsm_batch_metadata_block_hash(&changed));
363
364 let mut changed = base;
365 changed.directiveHash = FixedBytes::from([0x44; 32]);
366 assert_ne!(base_hash, rsm_batch_metadata_block_hash(&changed));
367 }
368
369 #[test]
370 fn rsm_batch_metadata_domain_changes_affect_signing_hash() {
371 let metadata = vector_metadata();
372 let base_hash = rsm_batch_metadata_signing_hash(
373 &metadata,
374 HYPERCALL_TESTNET_CHAIN_ID,
375 vector_contract(),
376 )
377 .unwrap();
378 let other_chain_hash = rsm_batch_metadata_signing_hash(
379 &metadata,
380 HYPERCALL_MAINNET_CHAIN_ID,
381 vector_contract(),
382 )
383 .unwrap();
384 let other_contract_hash = rsm_batch_metadata_signing_hash(
385 &metadata,
386 HYPERCALL_TESTNET_CHAIN_ID,
387 address!("0x2222222222222222222222222222222222222222"),
388 )
389 .unwrap();
390
391 assert_ne!(base_hash, other_chain_hash);
392 assert_ne!(base_hash, other_contract_hash);
393 }
394
395 #[test]
396 fn rsm_attestation_domain_rejects_invalid_inputs() {
397 assert_eq!(
398 rsm_attestation_domain(1, vector_contract()).unwrap_err(),
399 ValidatorRsmError::UnsupportedChainId(1)
400 );
401 assert_eq!(
402 rsm_attestation_domain(HYPERCALL_TESTNET_CHAIN_ID, Address::ZERO).unwrap_err(),
403 ValidatorRsmError::ZeroVerifyingContract
404 );
405 }
406}