Skip to main content

airbender_host/
program.rs

1use crate::error::{HostError, Result};
2#[cfg(feature = "gpu-prover")]
3use crate::prover::GpuProverBuilder;
4use crate::prover::{CpuProverBuilder, DevProverBuilder, ProverLevel};
5use crate::runner::TranspilerRunnerBuilder;
6use crate::verifier::{DevVerifierBuilder, RealVerifierBuilder};
7use airbender_core::host::manifest::Manifest;
8use sha2::Digest;
9use std::path::{Path, PathBuf};
10
11/// Loaded Airbender program distribution, including manifest and artifacts.
12#[derive(Clone, Debug)]
13pub struct Program {
14    dist_dir: PathBuf,
15    manifest: Manifest,
16    app_bin: PathBuf,
17    app_elf: PathBuf,
18    app_text: PathBuf,
19}
20
21impl Program {
22    pub fn load(dist_dir: impl AsRef<Path>) -> Result<Self> {
23        let dist_dir = dist_dir.as_ref().to_path_buf();
24        let manifest_path = dist_dir.join("manifest.toml");
25        let manifest = Manifest::read_from_file(&manifest_path)
26            .map_err(|err| HostError::InvalidManifest(err.to_string()))?;
27        let supported_codec = format!("v{}", airbender_codec::AIRBENDER_CODEC_V0);
28        if manifest.codec != supported_codec {
29            return Err(HostError::InvalidManifest(format!(
30                "unsupported codec `{}`",
31                manifest.codec
32            )));
33        }
34
35        let app_bin = dist_dir.join(&manifest.bin.path);
36        let app_elf = dist_dir.join(&manifest.elf.path);
37        let app_text = dist_dir.join(&manifest.text.path);
38
39        for path in [&app_bin, &app_elf, &app_text] {
40            if !path.exists() {
41                return Err(HostError::InvalidManifest(format!(
42                    "missing artifact: {}",
43                    path.display()
44                )));
45            }
46        }
47
48        verify_manifest_artifact_sha256(&app_bin, "bin.sha256", &manifest.bin.sha256)?;
49        verify_manifest_artifact_sha256(&app_elf, "elf.sha256", &manifest.elf.sha256)?;
50        verify_manifest_artifact_sha256(&app_text, "text.sha256", &manifest.text.sha256)?;
51
52        Ok(Self {
53            dist_dir,
54            manifest,
55            app_bin,
56            app_elf,
57            app_text,
58        })
59    }
60
61    pub fn dist_dir(&self) -> &Path {
62        &self.dist_dir
63    }
64
65    pub fn manifest(&self) -> &Manifest {
66        &self.manifest
67    }
68
69    pub fn app_bin(&self) -> &Path {
70        &self.app_bin
71    }
72
73    pub fn app_elf(&self) -> &Path {
74        &self.app_elf
75    }
76
77    pub fn app_text(&self) -> &Path {
78        &self.app_text
79    }
80
81    /// Create a transpiler runner builder bound to this program.
82    pub fn transpiler_runner(&self) -> TranspilerRunnerBuilder {
83        TranspilerRunnerBuilder::new(self.app_bin())
84    }
85
86    #[cfg(feature = "gpu-prover")]
87    /// Create a GPU prover builder bound to this program.
88    pub fn gpu_prover(&self) -> GpuProverBuilder {
89        GpuProverBuilder::new(self.app_bin())
90    }
91
92    /// Create a development prover builder bound to this program.
93    pub fn dev_prover(&self) -> DevProverBuilder {
94        DevProverBuilder::new(self.app_bin())
95    }
96
97    /// Create a CPU prover builder bound to this program.
98    pub fn cpu_prover(&self) -> CpuProverBuilder {
99        CpuProverBuilder::new(self.app_bin())
100    }
101
102    /// Create a development verifier builder bound to this program.
103    pub fn dev_verifier(&self) -> DevVerifierBuilder {
104        DevVerifierBuilder::new(self.app_bin())
105    }
106
107    /// Create a real verifier builder bound to this program.
108    pub fn real_verifier(&self, level: ProverLevel) -> RealVerifierBuilder {
109        RealVerifierBuilder::new(self.app_bin(), level)
110    }
111}
112
113fn verify_manifest_artifact_sha256(
114    path: &Path,
115    field_name: &str,
116    expected_hex: &str,
117) -> Result<()> {
118    if expected_hex.is_empty() {
119        return Err(HostError::InvalidManifest(format!(
120            "missing `{field_name}` in manifest; rebuild artifacts with current tooling"
121        )));
122    }
123
124    if expected_hex.len() != 64 || !expected_hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
125        return Err(HostError::InvalidManifest(format!(
126            "invalid `{field_name}` in manifest: `{expected_hex}`"
127        )));
128    }
129
130    let actual_hex = sha256_file_hex(path)?;
131    if !expected_hex.eq_ignore_ascii_case(&actual_hex) {
132        return Err(HostError::InvalidManifest(format!(
133            "`{field_name}` mismatch for {}: expected `{expected_hex}`, got `{actual_hex}`",
134            path.display()
135        )));
136    }
137
138    Ok(())
139}
140
141fn sha256_file_hex(path: &Path) -> Result<String> {
142    let bytes = std::fs::read(path)?;
143    let digest = sha2::Sha256::digest(bytes);
144    let mut encoded = String::with_capacity(digest.len() * 2);
145    for byte in digest {
146        use std::fmt::Write as _;
147        write!(&mut encoded, "{byte:02x}").expect("writing to string cannot fail");
148    }
149
150    Ok(encoded)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn verifies_matching_manifest_digest() {
159        let temp_file = unique_temp_file_path("matching-digest");
160        std::fs::write(&temp_file, b"hello world").expect("write test file");
161        let expected = sha256_file_hex(&temp_file).expect("compute expected digest");
162
163        verify_manifest_artifact_sha256(&temp_file, "bin.sha256", &expected)
164            .expect("digest verification must pass");
165
166        std::fs::remove_file(&temp_file).expect("remove test file");
167    }
168
169    #[test]
170    fn rejects_mismatching_manifest_digest() {
171        let temp_file = unique_temp_file_path("mismatching-digest");
172        std::fs::write(&temp_file, b"hello world").expect("write test file");
173        let wrong = "0000000000000000000000000000000000000000000000000000000000000000";
174
175        let err = verify_manifest_artifact_sha256(&temp_file, "bin.sha256", wrong)
176            .expect_err("digest verification must fail for mismatching hash");
177        assert!(err.to_string().contains("bin.sha256` mismatch"));
178
179        std::fs::remove_file(&temp_file).expect("remove test file");
180    }
181
182    #[test]
183    fn rejects_missing_manifest_digest() {
184        let temp_file = unique_temp_file_path("missing-digest");
185        std::fs::write(&temp_file, b"hello world").expect("write test file");
186
187        let err = verify_manifest_artifact_sha256(&temp_file, "bin.sha256", "")
188            .expect_err("digest verification must fail when digest is missing");
189        assert!(err.to_string().contains("missing `bin.sha256`"));
190
191        std::fs::remove_file(&temp_file).expect("remove test file");
192    }
193
194    fn unique_temp_file_path(label: &str) -> PathBuf {
195        let now = std::time::SystemTime::now()
196            .duration_since(std::time::UNIX_EPOCH)
197            .expect("system time must be after unix epoch")
198            .as_nanos();
199        std::env::temp_dir().join(format!(
200            "airbender-host-program-{label}-{}-{now}.bin",
201            std::process::id()
202        ))
203    }
204}