Skip to main content

hypercall/rsm/
accepted_state.rs

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(&current, 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, &current)?;
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}