Skip to main content

arete_server/
program_runtime.rs

1use anyhow::{bail, Result};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5pub type ProgramSpecHash = arete_hash::HashId<arete_hash::ProgramSpec>;
6pub type IdlContentHash = arete_hash::HashId<arete_hash::IdlContent>;
7pub type NormalizedIdlHash = arete_hash::HashId<arete_hash::IdlNormalized>;
8pub type ProgramReleaseHash = arete_hash::HashId<arete_hash::ProgramRelease>;
9
10/// A generated account decoder bound to one program release.
11///
12/// Macro-generated readers accept trailing account bytes, matching live decoder
13/// behavior, but never expose those bytes in their JSON result.
14pub type ProgramAccountReaderFn =
15    Arc<dyn Fn(&str, &[u8]) -> Result<serde_json::Value> + Send + Sync>;
16
17#[derive(Clone)]
18pub struct ProgramRuntimeDefinition {
19    pub program_id: String,
20    pub program_spec_hash: ProgramSpecHash,
21    pub idl_content_hash: IdlContentHash,
22    pub normalized_idl_hash: NormalizedIdlHash,
23    pub program_release_hash: ProgramReleaseHash,
24    pub account_reader: ProgramAccountReaderFn,
25}
26
27impl ProgramRuntimeDefinition {
28    fn validate(&self) -> Result<()> {
29        let expected = arete_hash::OssGeneratedProgramReleaseV1::new(
30            self.program_id.clone(),
31            self.program_spec_hash,
32            self.idl_content_hash,
33            self.normalized_idl_hash,
34        )
35        .hash()?;
36        if expected != self.program_release_hash {
37            bail!(
38                "program runtime definition release hash does not match its public identity fields"
39            );
40        }
41        Ok(())
42    }
43
44    fn is_exact_duplicate_of(&self, other: &Self) -> bool {
45        self.program_id == other.program_id
46            && self.program_spec_hash == other.program_spec_hash
47            && self.idl_content_hash == other.idl_content_hash
48            && self.normalized_idl_hash == other.normalized_idl_hash
49            && Arc::ptr_eq(&self.account_reader, &other.account_reader)
50    }
51}
52
53#[derive(Clone, Default)]
54pub struct ProgramRuntimeCatalog {
55    definitions: HashMap<ProgramReleaseHash, ProgramRuntimeDefinition>,
56}
57
58impl ProgramRuntimeCatalog {
59    pub fn try_new(definitions: Vec<ProgramRuntimeDefinition>) -> Result<Self> {
60        let mut catalog = Self::default();
61        for definition in definitions {
62            definition.validate()?;
63            if let Some(existing) = catalog.definitions.get(&definition.program_release_hash) {
64                if !existing.is_exact_duplicate_of(&definition) {
65                    bail!("conflicting program runtime definitions use the same release hash");
66                }
67                continue;
68            }
69            catalog
70                .definitions
71                .insert(definition.program_release_hash, definition);
72        }
73        Ok(catalog)
74    }
75
76    pub fn get(&self, release_hash: &ProgramReleaseHash) -> Option<&ProgramRuntimeDefinition> {
77        self.definitions.get(release_hash)
78    }
79
80    pub fn len(&self) -> usize {
81        self.definitions.len()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.definitions.is_empty()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn definition(program_id: &str, reader: ProgramAccountReaderFn) -> ProgramRuntimeDefinition {
94        let program_spec_hash = ProgramSpecHash::from_digest([1; 32]);
95        let idl_content_hash = IdlContentHash::from_digest([2; 32]);
96        let normalized_idl_hash = NormalizedIdlHash::from_digest([3; 32]);
97        let program_release_hash = arete_hash::OssGeneratedProgramReleaseV1::new(
98            program_id,
99            program_spec_hash,
100            idl_content_hash,
101            normalized_idl_hash,
102        )
103        .hash()
104        .unwrap();
105        ProgramRuntimeDefinition {
106            program_id: program_id.to_string(),
107            program_spec_hash,
108            idl_content_hash,
109            normalized_idl_hash,
110            program_release_hash,
111            account_reader: reader,
112        }
113    }
114
115    #[test]
116    fn catalog_accepts_exact_duplicates_and_rejects_conflicts() {
117        let reader: ProgramAccountReaderFn = Arc::new(|_, _| Ok(serde_json::Value::Null));
118        let first = definition("Program111", reader.clone());
119        let duplicate = first.clone();
120        assert_eq!(
121            ProgramRuntimeCatalog::try_new(vec![first.clone(), duplicate])
122                .unwrap()
123                .len(),
124            1
125        );
126
127        let mut conflict = first.clone();
128        conflict.account_reader = Arc::new(|_, _| Ok(serde_json::Value::Bool(true)));
129        assert!(ProgramRuntimeCatalog::try_new(vec![first, conflict]).is_err());
130    }
131
132    #[test]
133    fn catalog_rejects_a_release_hash_mismatch() {
134        let reader: ProgramAccountReaderFn = Arc::new(|_, _| Ok(serde_json::Value::Null));
135        let mut definition = definition("Program111", reader);
136        definition.program_release_hash = ProgramReleaseHash::from_digest([9; 32]);
137        assert!(ProgramRuntimeCatalog::try_new(vec![definition]).is_err());
138    }
139}