risc0_build_ethereum/
lib.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    env, fs,
17    io::Write,
18    path::{Path, PathBuf},
19    process::{Command, Stdio},
20};
21
22use anyhow::{anyhow, bail, Context, Result};
23use risc0_build::GuestListEntry;
24
25const SOL_HEADER: &str = r#"// Copyright 2024 RISC Zero, Inc.
26//
27// Licensed under the Apache License, Version 2.0 (the "License");
28// you may not use this file except in compliance with the License.
29// You may obtain a copy of the License at
30//
31//     http://www.apache.org/licenses/LICENSE-2.0
32//
33// Unless required by applicable law or agreed to in writing, software
34// distributed under the License is distributed on an "AS IS" BASIS,
35// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36// See the License for the specific language governing permissions and
37// limitations under the License.
38//
39// SPDX-License-Identifier: Apache-2.0
40
41// This file is automatically generated
42
43"#;
44
45const IMAGE_ID_LIB_HEADER: &str = r#"pragma solidity ^0.8.20;
46
47library ImageID {
48"#;
49
50const ELF_LIB_HEADER: &str = r#"pragma solidity ^0.8.20;
51
52library Elf {
53"#;
54
55/// Options for building and code generation.
56#[derive(Debug, Clone, Default)]
57#[non_exhaustive] // more options may be added in the future.
58pub struct Options {
59    /// Path the generated Solidity file with image ID information.
60    pub image_id_sol_path: Option<PathBuf>,
61
62    /// Path the generated Solidity file with ELF information.
63    pub elf_sol_path: Option<PathBuf>,
64}
65
66// Builder interface is provided to make it easy to add more intelligent default and additional
67// options without breaking backwards compatibility in the future.
68impl Options {
69    /// Add a path to generate the Solidity file with image ID information.
70    pub fn with_image_id_sol_path(mut self, path: impl AsRef<Path>) -> Self {
71        self.image_id_sol_path = Some(path.as_ref().to_owned());
72        self
73    }
74
75    /// Add a path to generate the Solidity file with ELF information.
76    pub fn with_elf_sol_path(mut self, path: impl AsRef<Path>) -> Self {
77        self.elf_sol_path = Some(path.as_ref().to_owned());
78        self
79    }
80}
81
82/// Generate Solidity files for integrating a RISC Zero project with Ethereum.
83pub fn generate_solidity_files(guests: &[GuestListEntry], opts: &Options) -> Result<()> {
84    // Skip Solidity source files generation if RISC0_SKIP_BUILD is enabled.
85    if env::var("RISC0_SKIP_BUILD").is_ok() {
86        return Ok(());
87    }
88    let image_id_file_path = opts
89        .image_id_sol_path
90        .as_ref()
91        .ok_or(anyhow!("path for image ID Solidity file must be provided"))?;
92    fs::write(image_id_file_path, generate_image_id_sol(guests)?)
93        .with_context(|| format!("failed to save changes to {}", image_id_file_path.display()))?;
94
95    let elf_sol_path = opts.elf_sol_path.as_ref().ok_or(anyhow!(
96        "path for guest ELFs Solidity file must be provided"
97    ))?;
98    fs::write(elf_sol_path, generate_elf_sol(guests)?)
99        .with_context(|| format!("failed to save changes to {}", image_id_file_path.display()))?;
100
101    Ok(())
102}
103
104/// Generate source code for a Solidity library containing image IDs for the given guest programs.
105pub fn generate_image_id_sol(guests: &[GuestListEntry]) -> Result<Vec<u8>> {
106    // Assemble a list of image IDs.
107    let image_ids: Vec<_> = guests
108        .iter()
109        .map(|guest| {
110            let name = guest.name.to_uppercase().replace('-', "_");
111            let image_id = guest.image_id;
112            format!("bytes32 public constant {name}_ID = bytes32(0x{image_id});")
113        })
114        .collect();
115
116    let image_id_lines = image_ids.join("\n");
117
118    // Building the final image_ID file content.
119    let file_content = format!("{SOL_HEADER}{IMAGE_ID_LIB_HEADER}\n{image_id_lines}\n}}");
120    forge_fmt(file_content.as_bytes()).context("failed to format image ID file")
121}
122
123/// Generate source code for a Solidity library containing local paths to the ELF files of guest
124/// programs. Note that these paths will only resolve on the build machine, and are intended only
125/// for test integration.
126pub fn generate_elf_sol(guests: &[GuestListEntry]) -> Result<Vec<u8>> {
127    // Assemble a list of paths to ELF files.
128    let elf_paths: Vec<_> = guests
129        .iter()
130        .map(|guest| {
131            let name = guest.name.to_uppercase().replace('-', "_");
132
133            let elf_path = guest.path.to_string();
134            format!("string public constant {name}_PATH = \"{elf_path}\";")
135        })
136        .collect();
137
138    // Building the final elf_path file content.
139    let elf_path_lines = elf_paths.join("\n");
140    let file_content = format!("{SOL_HEADER}{ELF_LIB_HEADER}\n{elf_path_lines}\n}}");
141    forge_fmt(file_content.as_bytes()).context("failed to format image ID file")
142}
143
144/// Uses forge fmt as a subprocess to format the given Solidity source.
145fn forge_fmt(src: &[u8]) -> Result<Vec<u8>> {
146    // Spawn `forge fmt`
147    let mut fmt_proc = Command::new("forge")
148        .args(["fmt", "-", "--raw"])
149        .stdin(Stdio::piped())
150        .stdout(Stdio::piped())
151        .spawn()
152        .context("failed to spawn forge fmt")?;
153
154    // Write the source code as bytes to stdin.
155    fmt_proc
156        .stdin
157        .take()
158        .context("failed to take forge fmt stdin handle")?
159        .write_all(src)
160        .context("failed to write to forge fmt stdin")?;
161
162    let fmt_out = fmt_proc
163        .wait_with_output()
164        .context("failed to run forge fmt")?;
165
166    if !fmt_out.status.success() {
167        bail!(
168            "forge fmt on image ID file content exited with status {}",
169            fmt_out.status,
170        );
171    }
172
173    Ok(fmt_out.stdout)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::forge_fmt;
179    use pretty_assertions::assert_eq;
180
181    // Copied from https://solidity-by-example.org/first-app/
182    const FORMATTED_SRC: &str = r#"// SPDX-License-Identifier: MIT
183pragma solidity ^0.8.20;
184
185contract Counter {
186    uint256 public count;
187
188    // Function to get the current count
189    function get() public view returns (uint256) {
190        return count;
191    }
192
193    // Function to increment count by 1
194    function inc() public {
195        count += 1;
196    }
197
198    // Function to decrement count by 1
199    function dec() public {
200        // This function will fail if count = 0
201        count -= 1;
202    }
203}
204"#;
205
206    const UNFORMATTED_SRC: &str = r#"
207// SPDX-License-Identifier: MIT
208pragma solidity ^0.8.20;
209
210contract Counter {
211    uint public  count;
212
213    // Function to get the current count
214    function get() public view returns (uint) {
215        return count;
216    }
217
218    // Function to increment count by 1
219    function inc()
220    public {
221        count
222         +=
223         1;
224    }
225
226// Function to decrement count by 1
227        function dec() public {
228            // This function will fail if count = 0
229count-=1;
230    }
231}
232
233"#;
234
235    #[test]
236    fn forge_fmt_works() {
237        assert_eq!(
238            String::from_utf8(forge_fmt(UNFORMATTED_SRC.as_bytes()).unwrap()).unwrap(),
239            String::from_utf8(FORMATTED_SRC.as_bytes().to_owned()).unwrap()
240        );
241    }
242}