1use alloy::primitives::keccak256;
2use anyhow::{bail, Context, Result};
3use hypercall_db::{
4 RsmBlockView, ValidatorRsmCurrentState, ValidatorRsmEnvironment, ValidatorRsmRootSummary,
5 ValidatorRsmStateAsyncReader, ValidatorRsmStateReader,
6};
7
8#[derive(Clone)]
9pub struct AcceptedRsmStateContext {
10 pub db: std::sync::Arc<dyn ValidatorRsmStateAsyncReader>,
11 pub environment: ValidatorRsmEnvironment,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RsmSubmissionClaim {
16 pub environment: ValidatorRsmEnvironment,
17 pub root_version: u64,
18 pub block_height: u64,
19 pub block_hash: [u8; 32],
20 pub parent_hash: [u8; 32],
21 pub commands_hash: [u8; 32],
22 pub batch_seq: u64,
23 pub batch_root: [u8; 32],
24 pub state_root: [u8; 32],
25 pub risk_root: [u8; 32],
26 pub command_mmr_root: [u8; 32],
27 pub obligation_mmr_root: [u8; 32],
28 pub intent_mmr_root: [u8; 32],
29 pub command_count: u64,
30 pub first_command_seq: u64,
31 pub last_command_seq: u64,
32 pub schema_version: i32,
33 pub directive_hash: [u8; 32],
34}
35
36impl RsmSubmissionClaim {
37 pub const PREIMAGE_PREFIX: &'static [u8] = b"hypercall-rsm-submission-claim-v1";
38
39 pub fn from_block(block: RsmBlockView, directive_hash: [u8; 32]) -> Result<Self> {
40 validate_accepted_block(&block)?;
41
42 Ok(Self {
43 environment: block.block.environment,
44 root_version: block.root_summary.version,
45 block_height: block.block.height,
46 block_hash: block.block.hash,
47 parent_hash: block.block.parent_hash,
48 commands_hash: block.block.commands_hash,
49 batch_seq: block.root_summary.batch_seq,
50 batch_root: block.root_summary.batch_root,
51 state_root: block.root_summary.state_root,
52 risk_root: block.root_summary.risk_root,
53 command_mmr_root: block.root_summary.command_mmr_root,
54 obligation_mmr_root: block.root_summary.obligation_mmr_root,
55 intent_mmr_root: block.root_summary.intent_mmr_root,
56 command_count: block.block.command_count,
57 first_command_seq: block.block.first_command_seq,
58 last_command_seq: block.block.last_command_seq,
59 schema_version: block.root_summary.schema_version,
60 directive_hash,
61 })
62 }
63
64 pub fn signing_preimage(&self) -> Vec<u8> {
65 let mut bytes = Vec::with_capacity(Self::PREIMAGE_PREFIX.len() + 2 + 8 * 6 + 32 * 10 + 4);
66 bytes.extend_from_slice(Self::PREIMAGE_PREFIX);
67 bytes.extend_from_slice(&(self.environment.as_i16()).to_be_bytes());
68 bytes.extend_from_slice(&self.root_version.to_be_bytes());
69 bytes.extend_from_slice(&self.block_height.to_be_bytes());
70 bytes.extend_from_slice(&self.block_hash);
71 bytes.extend_from_slice(&self.parent_hash);
72 bytes.extend_from_slice(&self.commands_hash);
73 bytes.extend_from_slice(&self.batch_seq.to_be_bytes());
74 bytes.extend_from_slice(&self.batch_root);
75 bytes.extend_from_slice(&self.state_root);
76 bytes.extend_from_slice(&self.risk_root);
77 bytes.extend_from_slice(&self.command_mmr_root);
78 bytes.extend_from_slice(&self.obligation_mmr_root);
79 bytes.extend_from_slice(&self.intent_mmr_root);
80 bytes.extend_from_slice(&self.command_count.to_be_bytes());
81 bytes.extend_from_slice(&self.first_command_seq.to_be_bytes());
82 bytes.extend_from_slice(&self.last_command_seq.to_be_bytes());
83 bytes.extend_from_slice(&self.schema_version.to_be_bytes());
84 bytes.extend_from_slice(&self.directive_hash);
85 bytes
86 }
87
88 pub fn claim_hash(&self) -> [u8; 32] {
89 keccak256(self.signing_preimage()).into()
90 }
91}
92
93pub async fn load_latest_submission_claim(
94 context: AcceptedRsmStateContext,
95 directive_hash: [u8; 32],
96) -> Result<RsmSubmissionClaim> {
97 load_latest_submission_claim_async(&*context.db, context.environment, directive_hash).await
98}
99
100pub async fn try_load_latest_accepted_block(
101 context: AcceptedRsmStateContext,
102) -> Result<Option<RsmBlockView>> {
103 try_load_latest_accepted_block_async(&*context.db, context.environment).await
104}
105
106pub async fn load_latest_submission_claim_async<R>(
107 reader: &R,
108 environment: ValidatorRsmEnvironment,
109 directive_hash: [u8; 32],
110) -> Result<RsmSubmissionClaim>
111where
112 R: ValidatorRsmStateAsyncReader + ?Sized,
113{
114 let block = load_latest_accepted_block_async(reader, environment).await?;
115 RsmSubmissionClaim::from_block(block, directive_hash)
116}
117
118pub async fn load_latest_accepted_block_async<R>(
119 reader: &R,
120 environment: ValidatorRsmEnvironment,
121) -> Result<RsmBlockView>
122where
123 R: ValidatorRsmStateAsyncReader + ?Sized,
124{
125 hypercall_rsm_rpc::load_latest_accepted_block(reader, environment).await
126}
127
128pub async fn try_load_latest_accepted_block_async<R>(
129 reader: &R,
130 environment: ValidatorRsmEnvironment,
131) -> Result<Option<RsmBlockView>>
132where
133 R: ValidatorRsmStateAsyncReader + ?Sized,
134{
135 hypercall_rsm_rpc::try_load_latest_accepted_block(reader, environment).await
136}
137
138pub fn load_latest_submission_claim_sync<R>(
139 reader: &R,
140 environment: ValidatorRsmEnvironment,
141 directive_hash: [u8; 32],
142) -> Result<RsmSubmissionClaim>
143where
144 R: ValidatorRsmStateReader + ?Sized,
145{
146 let block = load_latest_accepted_block_sync(reader, environment)?;
147 RsmSubmissionClaim::from_block(block, directive_hash)
148}
149
150pub fn load_latest_accepted_block_sync<R>(
151 reader: &R,
152 environment: ValidatorRsmEnvironment,
153) -> Result<RsmBlockView>
154where
155 R: ValidatorRsmStateReader + ?Sized,
156{
157 try_load_latest_accepted_block_sync(reader, environment)?
158 .with_context(|| format!("missing validator RSM current state for {environment}"))
159}
160
161pub fn try_load_latest_accepted_block_sync<R>(
162 reader: &R,
163 environment: ValidatorRsmEnvironment,
164) -> Result<Option<RsmBlockView>>
165where
166 R: ValidatorRsmStateReader + ?Sized,
167{
168 let current = reader.get_validator_rsm_current_state_sync(environment)?;
169 let Some(current) = current else {
170 return Ok(None);
171 };
172 validate_current_environment(¤t, environment)?;
173
174 let root = reader
175 .get_validator_rsm_root_summary_sync(environment, current.current_version)?
176 .with_context(|| {
177 format!(
178 "missing validator RSM root summary for {environment} version {}",
179 current.current_version
180 )
181 })?;
182 validate_root_for_current(&root, ¤t)?;
183
184 let block = reader
185 .get_rsm_block_by_hash_sync(environment, root.accepted_block_hash)?
186 .with_context(|| {
187 format!(
188 "missing accepted validator RSM block {} for {environment} version {}",
189 hex32(&root.accepted_block_hash),
190 current.current_version
191 )
192 })?;
193 validate_accepted_block(&block)?;
194 validate_block_matches_current_root(&block, &root)?;
195
196 Ok(Some(block))
197}
198
199fn validate_current_environment(
200 current: &ValidatorRsmCurrentState,
201 environment: ValidatorRsmEnvironment,
202) -> Result<()> {
203 if current.environment != environment {
204 bail!(
205 "validator RSM current state environment mismatch: expected {environment}, got {}",
206 current.environment
207 );
208 }
209 Ok(())
210}
211
212fn validate_root_for_current(
213 root: &ValidatorRsmRootSummary,
214 current: &ValidatorRsmCurrentState,
215) -> Result<()> {
216 if root.environment != current.environment {
217 bail!(
218 "validator RSM root environment mismatch: current {}, root {}",
219 current.environment,
220 root.environment
221 );
222 }
223 if root.version != current.current_version {
224 bail!(
225 "validator RSM root version mismatch: current {}, root {}",
226 current.current_version,
227 root.version
228 );
229 }
230 Ok(())
231}
232
233fn validate_accepted_block(block: &RsmBlockView) -> Result<()> {
234 if block.block.environment != block.root_summary.environment {
235 bail!(
236 "validator RSM block environment mismatch: block {}, root {}",
237 block.block.environment,
238 block.root_summary.environment
239 );
240 }
241 if block.block.height != block.root_summary.batch_seq {
242 bail!(
243 "validator RSM block/root height mismatch: block {}, root batch {}",
244 block.block.height,
245 block.root_summary.batch_seq
246 );
247 }
248 if block.block.hash != block.root_summary.accepted_block_hash {
249 bail!(
250 "validator RSM block/root hash mismatch: block {}, root accepted {}",
251 hex32(&block.block.hash),
252 hex32(&block.root_summary.accepted_block_hash)
253 );
254 }
255 if block.block.batch_root != block.root_summary.batch_root {
256 bail!(
257 "validator RSM block/root batch root mismatch: block {}, root {}",
258 hex32(&block.block.batch_root),
259 hex32(&block.root_summary.batch_root)
260 );
261 }
262 if block.block.command_count != block.root_summary.command_count {
263 bail!(
264 "validator RSM block/root command count mismatch: block {}, root {}",
265 block.block.command_count,
266 block.root_summary.command_count
267 );
268 }
269 if block.block.first_command_seq != block.root_summary.command_range_start {
270 bail!(
271 "validator RSM block/root first command mismatch: block {}, root {}",
272 block.block.first_command_seq,
273 block.root_summary.command_range_start
274 );
275 }
276 if block.block.last_command_seq != block.root_summary.command_range_end {
277 bail!(
278 "validator RSM block/root last command mismatch: block {}, root {}",
279 block.block.last_command_seq,
280 block.root_summary.command_range_end
281 );
282 }
283 Ok(())
284}
285
286fn validate_block_matches_current_root(
287 block: &RsmBlockView,
288 root: &ValidatorRsmRootSummary,
289) -> Result<()> {
290 if block.root_summary != *root {
291 bail!("validator RSM block root summary differs from current root summary");
292 }
293 Ok(())
294}
295
296fn hex32(bytes: &[u8; 32]) -> String {
297 format!("0x{}", hex::encode(bytes))
298}
299
300#[cfg(test)]
301mod tests {
302 use std::collections::HashMap;
303
304 use chrono::Utc;
305 use hypercall_db::{RsmBlockHeader, ValidatorRsmCurrentState};
306
307 use super::*;
308
309 #[derive(Default)]
310 struct FakeReader {
311 current: Option<ValidatorRsmCurrentState>,
312 roots: HashMap<u64, ValidatorRsmRootSummary>,
313 blocks: HashMap<[u8; 32], RsmBlockView>,
314 }
315
316 impl ValidatorRsmStateReader for FakeReader {
317 fn get_validator_rsm_root_summary_sync(
318 &self,
319 _environment: ValidatorRsmEnvironment,
320 version: u64,
321 ) -> Result<Option<ValidatorRsmRootSummary>> {
322 Ok(self.roots.get(&version).cloned())
323 }
324
325 fn get_validator_rsm_current_state_sync(
326 &self,
327 _environment: ValidatorRsmEnvironment,
328 ) -> Result<Option<ValidatorRsmCurrentState>> {
329 Ok(self.current.clone())
330 }
331
332 fn get_validator_rsm_current_root_summary_sync(
333 &self,
334 environment: ValidatorRsmEnvironment,
335 ) -> Result<Option<ValidatorRsmRootSummary>> {
336 let Some(current) = self.get_validator_rsm_current_state_sync(environment)? else {
337 return Ok(None);
338 };
339 self.get_validator_rsm_root_summary_sync(environment, current.current_version)
340 }
341
342 fn get_rsm_block_by_height_sync(
343 &self,
344 _environment: ValidatorRsmEnvironment,
345 height: u64,
346 ) -> Result<Option<RsmBlockView>> {
347 Ok(self
348 .blocks
349 .values()
350 .find(|block| block.block.height == height)
351 .cloned())
352 }
353
354 fn get_rsm_block_by_hash_sync(
355 &self,
356 _environment: ValidatorRsmEnvironment,
357 hash: [u8; 32],
358 ) -> Result<Option<RsmBlockView>> {
359 Ok(self.blocks.get(&hash).cloned())
360 }
361
362 fn get_latest_rsm_block_sync(
363 &self,
364 environment: ValidatorRsmEnvironment,
365 ) -> Result<Option<RsmBlockView>> {
366 let Some(root) = self.get_validator_rsm_current_root_summary_sync(environment)? else {
367 return Ok(None);
368 };
369 self.get_rsm_block_by_hash_sync(environment, root.accepted_block_hash)
370 }
371
372 fn list_rsm_blocks_sync(
373 &self,
374 _environment: ValidatorRsmEnvironment,
375 _from_height: Option<u64>,
376 _limit: u32,
377 ) -> Result<Vec<RsmBlockView>> {
378 Ok(self.blocks.values().cloned().collect())
379 }
380
381 fn get_rsm_block_commands_sync(
382 &self,
383 _environment: ValidatorRsmEnvironment,
384 _height: u64,
385 ) -> Result<Vec<hypercall_db::RsmBlockCommand>> {
386 Ok(Vec::new())
387 }
388 }
389
390 fn sample_reader() -> FakeReader {
391 let created_at = Utc::now();
392 let environment = ValidatorRsmEnvironment::Staging;
393 let root = ValidatorRsmRootSummary {
394 environment,
395 version: 9,
396 batch_seq: 7,
397 state_root: [7u8; 32],
398 risk_root: [8u8; 32],
399 command_mmr_root: [9u8; 32],
400 obligation_mmr_root: [10u8; 32],
401 intent_mmr_root: [11u8; 32],
402 batch_root: [4u8; 32],
403 command_range_start: 10,
404 command_range_end: 12,
405 command_count: 3,
406 schema_version: 1,
407 accepted_block_hash: [1u8; 32],
408 created_at,
409 };
410 let block = RsmBlockView {
411 block: RsmBlockHeader {
412 environment,
413 height: 7,
414 hash: [1u8; 32],
415 parent_hash: [2u8; 32],
416 commands_hash: [3u8; 32],
417 batch_root: [4u8; 32],
418 command_count: 3,
419 first_command_seq: 10,
420 last_command_seq: 12,
421 signer: Some([5u8; 20]),
422 signature: Some(vec![6u8; 65]),
423 created_at,
424 },
425 root_summary: root.clone(),
426 };
427
428 FakeReader {
429 current: Some(ValidatorRsmCurrentState {
430 environment,
431 current_version: root.version,
432 updated_at: created_at,
433 }),
434 roots: HashMap::from([(root.version, root)]),
435 blocks: HashMap::from([([1u8; 32], block)]),
436 }
437 }
438
439 #[test]
440 fn latest_submission_claim_uses_current_root_and_block() {
441 let claim = load_latest_submission_claim_sync(
442 &sample_reader(),
443 ValidatorRsmEnvironment::Staging,
444 [99u8; 32],
445 )
446 .unwrap();
447
448 assert_eq!(claim.environment, ValidatorRsmEnvironment::Staging);
449 assert_eq!(claim.root_version, 9);
450 assert_eq!(claim.block_height, 7);
451 assert_eq!(claim.block_hash, [1u8; 32]);
452 assert_eq!(claim.batch_root, [4u8; 32]);
453 assert_eq!(claim.state_root, [7u8; 32]);
454 assert_eq!(claim.directive_hash, [99u8; 32]);
455 }
456
457 #[test]
458 fn latest_submission_claim_fails_without_current_pointer() {
459 let mut reader = sample_reader();
460 reader.current = None;
461
462 let err = load_latest_submission_claim_sync(
463 &reader,
464 ValidatorRsmEnvironment::Staging,
465 [99u8; 32],
466 )
467 .unwrap_err();
468
469 assert!(err
470 .to_string()
471 .contains("missing validator RSM current state"));
472 }
473
474 #[test]
475 fn latest_submission_claim_fails_on_root_block_mismatch() {
476 let mut reader = sample_reader();
477 let block = reader.blocks.get_mut(&[1u8; 32]).unwrap();
478 block.root_summary.state_root = [42u8; 32];
479
480 let err = load_latest_submission_claim_sync(
481 &reader,
482 ValidatorRsmEnvironment::Staging,
483 [99u8; 32],
484 )
485 .unwrap_err();
486
487 assert!(err
488 .to_string()
489 .contains("block root summary differs from current root summary"));
490 }
491
492 #[test]
493 fn submission_claim_preimage_and_hash_are_stable() {
494 let claim = load_latest_submission_claim_sync(
495 &sample_reader(),
496 ValidatorRsmEnvironment::Staging,
497 [99u8; 32],
498 )
499 .unwrap();
500
501 assert_eq!(
502 &claim.signing_preimage()[..RsmSubmissionClaim::PREIMAGE_PREFIX.len()],
503 RsmSubmissionClaim::PREIMAGE_PREFIX
504 );
505 assert_eq!(
506 hex::encode(claim.claim_hash()),
507 "04ab8af05df9b2c817cbe7180a5b260171590eb2d3dae37a8d8b5f4525966551"
508 );
509 }
510}