Skip to main content

revmc_runtime/runtime/
storage.rs

1//! Artifact storage trait and data model.
2
3use crate::{OptimizationLevel, eyre};
4use alloy_primitives::B256;
5use dashmap::DashMap;
6use revm_primitives::hardfork::SpecId;
7use std::{fs, path::PathBuf};
8
9/// Runtime cache key: the minimal identity for a compiled program at runtime.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct RuntimeCacheKey {
12    /// The code hash of the contract bytecode.
13    pub code_hash: B256,
14    /// The EVM spec (hardfork) the code was compiled against.
15    pub spec_id: SpecId,
16}
17
18/// Full artifact identity for persisted artifacts.
19///
20/// Persisted artifacts must match all fields of this key to be loaded.
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub struct ArtifactKey {
23    /// The runtime cache key (code_hash + spec_id).
24    pub runtime: RuntimeCacheKey,
25    /// The compiler backend used.
26    pub backend: BackendSelection,
27    /// The optimization level used.
28    pub opt_level: OptimizationLevel,
29}
30
31/// A stored artifact consisting of a manifest and a path to the compiled dylib.
32#[derive(Clone, Debug)]
33pub struct StoredArtifact {
34    /// Metadata about the artifact.
35    pub manifest: ArtifactManifest,
36    /// Path to the shared library on disk. The store owns and manages these files.
37    pub dylib_path: PathBuf,
38}
39
40/// Metadata for a stored artifact.
41#[derive(Clone, Debug)]
42pub struct ArtifactManifest {
43    /// The full artifact key.
44    pub artifact_key: ArtifactKey,
45    /// The symbol name to look up in the loaded library.
46    pub symbol_name: String,
47    /// Length of the original bytecode.
48    pub bytecode_len: usize,
49    /// Length of the compiled artifact in bytes.
50    pub artifact_len: usize,
51    /// Creation timestamp (unix seconds).
52    pub created_at_unix_secs: u64,
53    /// Keccak-256 digest of the dylib bytes.
54    pub content_hash: [u8; 32],
55}
56
57/// Backend selection for compilation.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59pub enum BackendSelection {
60    /// Automatically select the best available backend.
61    #[default]
62    Auto,
63    /// Use the LLVM backend.
64    Llvm,
65}
66
67/// Trait for loading and storing compiled artifacts.
68///
69/// Implementations manage artifact files on the filesystem. The store owns the dylib files and
70/// returns paths to them. The backend loads shared libraries directly from these paths.
71pub trait ArtifactStore: Send + Sync + 'static {
72    /// Loads all available artifacts from storage.
73    fn load_all(&self) -> eyre::Result<Vec<(ArtifactKey, StoredArtifact)>>;
74
75    /// Loads a single artifact by key.
76    fn load(&self, key: &ArtifactKey) -> eyre::Result<Option<StoredArtifact>>;
77
78    /// Stores an artifact. The `dylib_bytes` are the raw shared-library bytes to persist.
79    /// The store writes them to disk and the returned path (via subsequent `load`) points there.
80    fn store(
81        &self,
82        key: &ArtifactKey,
83        manifest: &ArtifactManifest,
84        dylib_bytes: &[u8],
85    ) -> eyre::Result<()>;
86
87    /// Deletes an artifact by key.
88    fn delete(&self, key: &ArtifactKey) -> eyre::Result<()>;
89
90    /// Clears all stored artifacts.
91    fn clear(&self) -> eyre::Result<()>;
92}
93
94/// In-memory artifact index backed by dylib files in a temporary directory.
95#[derive(Debug)]
96pub struct RuntimeArtifactStore {
97    dir: tempfile::TempDir,
98    artifacts: DashMap<ArtifactKey, StoredArtifact>,
99}
100
101impl RuntimeArtifactStore {
102    /// Creates an empty temporary runtime artifact store.
103    pub fn new() -> eyre::Result<Self> {
104        Ok(Self { dir: tempfile::tempdir()?, artifacts: DashMap::default() })
105    }
106
107    /// Returns the number of artifacts tracked by this store.
108    pub fn len(&self) -> usize {
109        self.artifacts.len()
110    }
111
112    /// Returns whether this store contains no artifacts.
113    pub fn is_empty(&self) -> bool {
114        self.len() == 0
115    }
116
117    fn artifact_path(&self, key: &ArtifactKey) -> PathBuf {
118        self.dir.path().join(format!(
119            "{:x}_{:?}_{:?}_{:?}.so",
120            key.runtime.code_hash, key.runtime.spec_id, key.backend, key.opt_level,
121        ))
122    }
123}
124
125impl ArtifactStore for RuntimeArtifactStore {
126    fn load_all(&self) -> eyre::Result<Vec<(ArtifactKey, StoredArtifact)>> {
127        Ok(self
128            .artifacts
129            .iter()
130            .map(|entry| (entry.key().clone(), entry.value().clone()))
131            .collect())
132    }
133
134    fn load(&self, key: &ArtifactKey) -> eyre::Result<Option<StoredArtifact>> {
135        Ok(self.artifacts.get(key).map(|entry| entry.value().clone()))
136    }
137
138    fn store(
139        &self,
140        key: &ArtifactKey,
141        manifest: &ArtifactManifest,
142        dylib_bytes: &[u8],
143    ) -> eyre::Result<()> {
144        let path = self.artifact_path(key);
145        fs::write(&path, dylib_bytes)?;
146        self.artifacts
147            .insert(key.clone(), StoredArtifact { manifest: manifest.clone(), dylib_path: path });
148        Ok(())
149    }
150
151    fn delete(&self, key: &ArtifactKey) -> eyre::Result<()> {
152        if let Some((_, artifact)) = self.artifacts.remove(key) {
153            let _ = fs::remove_file(artifact.dylib_path);
154        }
155        Ok(())
156    }
157
158    fn clear(&self) -> eyre::Result<()> {
159        let paths = self.artifacts.iter().map(|entry| entry.dylib_path.clone()).collect::<Vec<_>>();
160        self.artifacts.clear();
161        for path in paths {
162            let _ = fs::remove_file(path);
163        }
164        Ok(())
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::{
171        ArtifactKey, ArtifactManifest, ArtifactStore, BackendSelection, RuntimeArtifactStore,
172        RuntimeCacheKey,
173    };
174    use alloy_primitives::B256;
175    use revm_primitives::hardfork::SpecId;
176    use revmc_backend::OptimizationLevel;
177
178    fn artifact_key(code_hash: B256) -> ArtifactKey {
179        ArtifactKey {
180            runtime: RuntimeCacheKey { code_hash, spec_id: SpecId::OSAKA },
181            backend: BackendSelection::Llvm,
182            opt_level: OptimizationLevel::Default,
183        }
184    }
185
186    fn manifest(key: ArtifactKey, artifact_len: usize) -> ArtifactManifest {
187        ArtifactManifest {
188            artifact_key: key,
189            symbol_name: "main".to_string(),
190            bytecode_len: 3,
191            artifact_len,
192            created_at_unix_secs: 42,
193            content_hash: [7; 32],
194        }
195    }
196
197    #[test]
198    fn runtime_artifact_store_starts_empty() {
199        let store = RuntimeArtifactStore::new().unwrap();
200
201        assert!(store.is_empty());
202        assert_eq!(store.len(), 0);
203        assert!(store.load_all().unwrap().is_empty());
204    }
205
206    #[test]
207    fn runtime_artifact_store_round_trips_artifacts() {
208        let store = RuntimeArtifactStore::new().unwrap();
209        let key = artifact_key(B256::with_last_byte(1));
210        let manifest = manifest(key.clone(), 4);
211
212        store.store(&key, &manifest, b"dylib").unwrap();
213
214        assert_eq!(store.len(), 1);
215        let loaded = store.load(&key).unwrap().unwrap();
216        assert_eq!(loaded.manifest.symbol_name, "main");
217        assert_eq!(loaded.manifest.artifact_len, 4);
218        assert_eq!(std::fs::read(&loaded.dylib_path).unwrap(), b"dylib");
219
220        let all = store.load_all().unwrap();
221        assert_eq!(all.len(), 1);
222        assert_eq!(all[0].0, key);
223        assert_eq!(all[0].1.manifest.content_hash, [7; 32]);
224    }
225
226    #[test]
227    fn runtime_artifact_store_replaces_artifacts() {
228        let store = RuntimeArtifactStore::new().unwrap();
229        let key = artifact_key(B256::with_last_byte(2));
230
231        store.store(&key, &manifest(key.clone(), 3), b"one").unwrap();
232        store.store(&key, &manifest(key.clone(), 5), b"three").unwrap();
233
234        assert_eq!(store.len(), 1);
235        let loaded = store.load(&key).unwrap().unwrap();
236        assert_eq!(loaded.manifest.artifact_len, 5);
237        assert_eq!(std::fs::read(&loaded.dylib_path).unwrap(), b"three");
238    }
239
240    #[test]
241    fn runtime_artifact_store_delete_and_clear_remove_files() {
242        let store = RuntimeArtifactStore::new().unwrap();
243        let first = artifact_key(B256::with_last_byte(3));
244        let second = artifact_key(B256::with_last_byte(4));
245
246        store.store(&first, &manifest(first.clone(), 1), b"a").unwrap();
247        store.store(&second, &manifest(second.clone(), 1), b"b").unwrap();
248        let first_path = store.load(&first).unwrap().unwrap().dylib_path;
249        let second_path = store.load(&second).unwrap().unwrap().dylib_path;
250
251        store.delete(&first).unwrap();
252        assert!(store.load(&first).unwrap().is_none());
253        assert!(!first_path.exists());
254        assert!(second_path.exists());
255
256        store.clear().unwrap();
257        assert!(store.is_empty());
258        assert!(!second_path.exists());
259    }
260
261    #[test]
262    fn backend_selection_defaults_to_auto() {
263        assert_eq!(BackendSelection::default(), BackendSelection::Auto);
264    }
265}