airbender_host/
program.rs1use 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#[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 pub fn transpiler_runner(&self) -> TranspilerRunnerBuilder {
83 TranspilerRunnerBuilder::new(self.app_bin())
84 }
85
86 #[cfg(feature = "gpu-prover")]
87 pub fn gpu_prover(&self) -> GpuProverBuilder {
89 GpuProverBuilder::new(self.app_bin())
90 }
91
92 pub fn dev_prover(&self) -> DevProverBuilder {
94 DevProverBuilder::new(self.app_bin())
95 }
96
97 pub fn cpu_prover(&self) -> CpuProverBuilder {
99 CpuProverBuilder::new(self.app_bin())
100 }
101
102 pub fn dev_verifier(&self) -> DevVerifierBuilder {
104 DevVerifierBuilder::new(self.app_bin())
105 }
106
107 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}