1
use std::fs::{self, File};
2
use std::path::Path;
3
use std::process::Command;
4
use std::time::Duration;
5

            
6
use anyhow::Result;
7
use cargo_toml::Manifest;
8
use indicatif::ProgressBar;
9
use risc0_zkvm::compute_image_id;
10
use solana_sdk::signer::Signer;
11

            
12
use crate::common::*;
13
use crate::error::{BonsolCliError, ZkManifestError};
14

            
15
pub fn build(keypair: &impl Signer, zk_program_path: String) -> Result<()> {
16
    validate_build_dependencies()?;
17

            
18
    let bar = ProgressBar::new_spinner();
19
    bar.enable_steady_tick(Duration::from_millis(100));
20

            
21
    let image_path = Path::new(&zk_program_path);
22
    let (cargo_package_name, input_order) = parse_cargo_manifest(image_path)?;
23
    let build_result =
24
        build_zkprogram_manifest(image_path, &keypair, cargo_package_name, input_order);
25
    let manifest_path = image_path.join(MANIFEST_JSON);
26

            
27
    match build_result {
28
        Err(e) => {
29
            bar.finish_with_message(format!(
30
                "Build failed for program '{}': {:?}",
31
                image_path.to_string_lossy(),
32
                e
33
            ));
34
            Ok(())
35
        }
36
        Ok(manifest) => {
37
            serde_json::to_writer_pretty(File::create(&manifest_path)?, &manifest)?;
38
            bar.finish_and_clear();
39
            println!("Build complete");
40
            Ok(())
41
        }
42
    }
43
}
44

            
45
fn check_cargo_risczero_version() -> Result<(), BonsolCliError> {
46
    let output = Command::new(CARGO_COMMAND)
47
        .args(["risczero", "--version"])
48
        .output()
49
        .map_err(|e| {
50
            BonsolCliError::BuildFailure(format!("Failed to get cargo-risczero version: {:?}", e))
51
        })?;
52
    if output.status.success() {
53
        let version = String::from_utf8(output.stdout).map_err(|e| {
54
            BonsolCliError::BuildFailure(format!("Failed to parse cargo-risczero version: {:?}", e))
55
        })?;
56
        if !version.contains(CARGO_RISCZERO_VERSION) {
57
            return Err(BonsolCliError::BuildDependencyVersionMismatch {
58
                dep: "cargo-risczero".to_string(),
59
                version: CARGO_RISCZERO_VERSION.to_string(),
60
                current_version: version,
61
            });
62
        }
63
    }
64
    Ok(())
65
}
66

            
67
fn validate_build_dependencies() -> Result<(), BonsolCliError> {
68
    const CARGO_RISCZERO: &str = "risczero";
69
    const DOCKER: &str = "docker";
70

            
71
    let mut missing_deps = Vec::with_capacity(2);
72

            
73
    if !cargo_has_plugin(CARGO_RISCZERO) {
74
        missing_deps.push(format!("cargo-{}", CARGO_RISCZERO));
75
    }
76

            
77
    if !has_executable(DOCKER) {
78
        missing_deps.push(DOCKER.into());
79
    }
80

            
81
    if !missing_deps.is_empty() {
82
        return Err(BonsolCliError::MissingBuildDependencies { missing_deps });
83
    }
84

            
85
    check_cargo_risczero_version()?;
86

            
87
    Ok(())
88
}
89

            
90
fn parse_cargo_manifest_inputs(
91
    manifest: &Manifest,
92
    manifest_path_str: String,
93
) -> Result<Vec<String>> {
94
    const METADATA: &str = "metadata";
95
    const ZKPROGRAM: &str = "zkprogram";
96
    const INPUT_ORDER: &str = "input_order";
97

            
98
    let meta = manifest
99
        .package
100
        .as_ref()
101
        .and_then(|p| p.metadata.as_ref())
102
        .ok_or(ZkManifestError::MissingPackageMetadata(
103
            manifest_path_str.clone(),
104
        ))?;
105
    let meta_table = meta.as_table().ok_or(ZkManifestError::ExpectedTable {
106
        manifest_path: manifest_path_str.clone(),
107
        name: METADATA.into(),
108
    })?;
109
    let zkprogram = meta_table
110
        .get(ZKPROGRAM)
111
        .ok_or(ZkManifestError::MissingProgramMetadata {
112
            manifest_path: manifest_path_str.clone(),
113
            meta: meta.to_owned(),
114
        })?;
115
    let zkprogram_table = zkprogram.as_table().ok_or(ZkManifestError::ExpectedTable {
116
        manifest_path: manifest_path_str.clone(),
117
        name: ZKPROGRAM.into(),
118
    })?;
119
    let input_order =
120
        zkprogram_table
121
            .get(INPUT_ORDER)
122
            .ok_or(ZkManifestError::MissingInputOrder {
123
                manifest_path: manifest_path_str.clone(),
124
                zkprogram: zkprogram.to_owned(),
125
            })?;
126
    let inputs = input_order
127
        .as_array()
128
        .ok_or(ZkManifestError::ExpectedArray {
129
            manifest_path: manifest_path_str.clone(),
130
            name: INPUT_ORDER.into(),
131
        })?;
132

            
133
    let (input_order, errs): (
134
        Vec<Result<String, ZkManifestError>>,
135
        Vec<Result<String, ZkManifestError>>,
136
    ) = inputs
137
        .iter()
138
        .map(|i| -> Result<String, ZkManifestError> {
139
            i.as_str()
140
                .map(|s| s.to_string())
141
                .ok_or(ZkManifestError::InvalidInput(i.to_owned()))
142
        })
143
        .partition(|res| res.is_ok());
144
    if !errs.is_empty() {
145
        let errs: Vec<String> = errs
146
            .into_iter()
147
            .map(|r| format!("Error: {:?}\n", r.unwrap_err()))
148
            .collect();
149
        return Err(ZkManifestError::InvalidInputs {
150
            manifest_path: manifest_path_str,
151
            errs,
152
        }
153
        .into());
154
    }
155

            
156
    Ok(input_order.into_iter().map(Result::unwrap).collect())
157
}
158

            
159
fn parse_cargo_manifest(image_path: &Path) -> Result<(String, Vec<String>)> {
160
    let cargo_manifest_path = image_path.join(CARGO_TOML);
161
    let cargo_manifest_path_str = cargo_manifest_path.to_string_lossy().to_string();
162
    if !cargo_manifest_path.exists() {
163
        return Err(
164
            ZkManifestError::MissingManifest(image_path.to_string_lossy().to_string()).into(),
165
        );
166
    }
167
    let cargo_manifest = cargo_toml::Manifest::from_path(&cargo_manifest_path).map_err(|err| {
168
        ZkManifestError::FailedToLoadManifest {
169
            manifest_path: cargo_manifest_path_str.clone(),
170
            err,
171
        }
172
    })?;
173
    let cargo_package_name = cargo_manifest
174
        .package
175
        .as_ref()
176
        .map(|p| p.name.clone())
177
        .ok_or(ZkManifestError::MissingPackageName(
178
            cargo_manifest_path_str.clone(),
179
        ))?;
180
    let input_order = parse_cargo_manifest_inputs(&cargo_manifest, cargo_manifest_path_str)?;
181

            
182
    Ok((cargo_package_name, input_order))
183
}
184

            
185
fn build_zkprogram_manifest(
186
    image_path: &Path,
187
    keypair: &impl Signer,
188
    cargo_package_name: String,
189
    input_order: Vec<String>,
190
) -> Result<ZkProgramManifest> {
191
    const RISCV_DOCKER_PATH: &str = "target/riscv-guest/riscv32im-risc0-zkvm-elf/docker";
192
    const CARGO_RISCZERO_BUILD_ARGS: &[&str; 4] =
193
        &["risczero", "build", "--manifest-path", "Cargo.toml"];
194

            
195
    let binary_path = image_path
196
        .join(RISCV_DOCKER_PATH)
197
        .join(&cargo_package_name)
198
        .join(&cargo_package_name);
199
    let output = Command::new(CARGO_COMMAND)
200
        .current_dir(image_path)
201
        .args(CARGO_RISCZERO_BUILD_ARGS)
202
        .env("CARGO_TARGET_DIR", image_path.join(TARGET_DIR))
203
        .output()?;
204

            
205
    if output.status.success() {
206
        let elf_contents = fs::read(&binary_path)?;
207
        let image_id = compute_image_id(&elf_contents).map_err(|err| {
208
            BonsolCliError::FailedToComputeImageId {
209
                binary_path: binary_path.to_string_lossy().to_string(),
210
                err,
211
            }
212
        })?;
213
        let signature = keypair.sign_message(elf_contents.as_slice());
214
        let zkprogram_manifest = ZkProgramManifest {
215
            name: cargo_package_name,
216
            binary_path: binary_path
217
                .to_str()
218
                .ok_or(ZkManifestError::InvalidBinaryPath)?
219
                .to_string(),
220
            input_order,
221
            image_id: image_id.to_string(),
222
            size: elf_contents.len() as u64,
223
            signature: signature.to_string(),
224
        };
225
        return Ok(zkprogram_manifest);
226
    }
227

            
228
    Err(BonsolCliError::BuildFailure(String::from_utf8_lossy(&output.stderr).to_string()).into())
229
}