Skip to main content

linera_execution/test_utils/
solidity.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Code for compiling solidity smart contracts for testing purposes.
5
6use std::{
7    fs::File,
8    io::Write,
9    path::{Path, PathBuf},
10    process::{Command, Stdio},
11};
12
13use anyhow::Context;
14use revm_primitives::{Address, U256};
15use serde_json::Value;
16use tempfile::{tempdir, TempDir};
17
18use crate::{LINERA_SOL, LINERA_TYPES_SOL};
19
20fn write_compilation_json(
21    path: &Path,
22    file_name: &str,
23    optimizer_runs: Option<u32>,
24) -> anyhow::Result<()> {
25    let optimizer_block = optimizer_runs
26        .map(|runs| {
27            format!(
28                r#""optimizer": {{
29                    "enabled": true,
30                    "runs": {runs}
31                }},
32                "#
33            )
34        })
35        .unwrap_or_default();
36    let mut source = File::create(path).unwrap();
37    writeln!(
38        source,
39        r#"
40{{
41  "language": "Solidity",
42  "sources": {{
43    "{file_name}": {{
44      "urls": ["./{file_name}"]
45    }}
46  }},
47  "settings": {{
48    "viaIR": true,
49    "evmVersion": "cancun",
50    {optimizer_block}"outputSelection": {{
51      "*": {{
52        "*": ["evm.bytecode"]
53      }}
54    }}
55  }}
56}}
57"#
58    )?;
59    Ok(())
60}
61
62fn get_bytecode_path(
63    path: &Path,
64    file_name: &str,
65    contract_name: &str,
66    optimizer_runs: Option<u32>,
67) -> anyhow::Result<Vec<u8>> {
68    let config_path = path.join("config.json");
69    write_compilation_json(&config_path, file_name, optimizer_runs)?;
70    let config_file = File::open(config_path)?;
71
72    let output_path = path.join("result.json");
73    let output_file = File::create(output_path.clone())?;
74
75    let status = Command::new("solc")
76        .current_dir(path)
77        .arg("--standard-json")
78        .stdin(Stdio::from(config_file))
79        .stdout(Stdio::from(output_file))
80        .status()?;
81    assert!(status.success());
82
83    let contents = std::fs::read_to_string(output_path)?;
84    let json_data: serde_json::Value = serde_json::from_str(&contents)?;
85    let contracts = json_data
86        .get("contracts")
87        .with_context(|| format!("failed to get contracts in json_data={json_data}"))?;
88    let file_name_contract = contracts
89        .get(file_name)
90        .context("failed to get {file_name}")?;
91    let test_data = file_name_contract
92        .get(contract_name)
93        .with_context(|| format!("failed to get contract_name={contract_name}"))?;
94    let evm_data = test_data
95        .get("evm")
96        .with_context(|| format!("failed to get evm in test_data={test_data}"))?;
97    let bytecode = evm_data
98        .get("bytecode")
99        .with_context(|| format!("failed to get bytecode in evm_data={evm_data}"))?;
100    let object = bytecode
101        .get("object")
102        .with_context(|| format!("failed to get object in bytecode={bytecode}"))?;
103    let object = object.to_string();
104    let object = object.trim_matches(|c| c == '"').to_string();
105    Ok(hex::decode(&object)?)
106}
107
108/// Compiles a Solidity contract and returns the bytecode of the named contract.
109pub fn compile_solidity_contract(
110    source_code: &str,
111    file_name: &str,
112    contract_name: &str,
113    extra_sources: &[(&str, &str)],
114) -> anyhow::Result<Vec<u8>> {
115    compile_solidity_contract_with_options(
116        source_code,
117        file_name,
118        contract_name,
119        extra_sources,
120        None,
121    )
122}
123
124/// Compiles a Solidity contract with the given optimizer settings, returning the contract's
125/// bytecode.
126pub fn compile_solidity_contract_with_options(
127    source_code: &str,
128    file_name: &str,
129    contract_name: &str,
130    extra_sources: &[(&str, &str)],
131    optimizer_runs: Option<u32>,
132) -> anyhow::Result<Vec<u8>> {
133    let dir = tempdir().unwrap();
134    let path = dir.path();
135    for (extra_file_name, extra_source_code) in extra_sources {
136        let extra_code_path = path.join(extra_file_name);
137        let mut extra_code_file = File::create(&extra_code_path)?;
138        writeln!(extra_code_file, "{extra_source_code}")?;
139    }
140    if source_code.contains("Linera.sol") {
141        // The source code seems to import Linera.sol, so we import the relevant files.
142        for (file_name, literal_path) in [
143            ("Linera.sol", LINERA_SOL),
144            ("LineraTypes.sol", LINERA_TYPES_SOL),
145        ] {
146            let test_code_path = path.join(file_name);
147            let mut test_code_file = File::create(&test_code_path)?;
148            writeln!(test_code_file, "{literal_path}")?;
149        }
150    }
151    if source_code.contains("@openzeppelin") {
152        let _output = Command::new("npm")
153            .args(["install", "@openzeppelin/contracts"])
154            .current_dir(path)
155            .output()?;
156        let _output = Command::new("mv")
157            .args(["node_modules/@openzeppelin", "@openzeppelin"])
158            .current_dir(path)
159            .output()?;
160    }
161    let test_code_path = path.join(file_name);
162    let mut test_code_file = File::create(&test_code_path)?;
163    writeln!(test_code_file, "{source_code}")?;
164    get_bytecode_path(path, file_name, contract_name, optimizer_runs)
165}
166
167/// Compiles the given Solidity source code and returns the bytecode of the named contract.
168pub fn get_bytecode(source_code: &str, contract_name: &str) -> anyhow::Result<Vec<u8>> {
169    compile_solidity_contract(source_code, "test_code.sol", contract_name, &[])
170}
171
172/// Reads a Solidity file, detects its contract name, and returns the compiled bytecode.
173pub fn load_solidity_example(path: &str) -> anyhow::Result<Vec<u8>> {
174    let source_code = std::fs::read_to_string(path)?;
175    let contract_name: &str = source_code
176        .lines()
177        .find_map(|line| line.trim_start().strip_prefix("contract "))
178        .ok_or_else(|| anyhow::anyhow!("Not matching"))?;
179    let contract_name: &str = contract_name
180        .split_whitespace()
181        .next()
182        .ok_or_else(|| anyhow::anyhow!("No space found after the contract name"))?;
183    tracing::info!("load_solidity_example, contract_name={contract_name}");
184    get_bytecode(&source_code, contract_name)
185}
186
187/// Reads a Solidity file and returns the compiled bytecode of the contract with the given name.
188pub fn load_solidity_example_by_name(path: &str, contract_name: &str) -> anyhow::Result<Vec<u8>> {
189    let source_code = std::fs::read_to_string(path)?;
190    get_bytecode(&source_code, contract_name)
191}
192
193/// Writes an EVM module to a temporary file, returning its path and the owning temporary directory.
194pub fn temporary_write_evm_module(module: &[u8]) -> anyhow::Result<(PathBuf, TempDir)> {
195    let dir = tempfile::tempdir()?;
196    let path = dir.path();
197    let app_file = "app.json";
198    let app_path = path.join(app_file);
199    {
200        std::fs::write(app_path.clone(), module)?;
201    }
202    let evm_contract = app_path.to_path_buf();
203    Ok((evm_contract, dir))
204}
205
206/// Compiles a Solidity example and writes it to a temporary EVM module file, returning its path.
207pub fn get_evm_contract_path(path: &str) -> anyhow::Result<(PathBuf, TempDir)> {
208    let module = load_solidity_example(path)?;
209    temporary_write_evm_module(&module)
210}
211
212/// Converts a JSON array of byte values into a vector of bytes.
213pub fn value_to_vec_u8(value: &Value) -> Vec<u8> {
214    let mut vec: Vec<u8> = Vec::new();
215    for val in value.as_array().unwrap() {
216        let val = val.as_u64().unwrap();
217        let val = val as u8;
218        vec.push(val);
219    }
220    vec
221}
222
223/// Reads a `u64` from the last 8 bytes of a 32-byte EVM word encoded as a JSON byte array.
224pub fn read_evm_u64_entry(value: &Value) -> u64 {
225    let vec = value_to_vec_u8(value);
226    let mut arr = [0_u8; 8];
227    arr.copy_from_slice(&vec[24..]);
228    u64::from_be_bytes(arr)
229}
230
231/// Reads a `U256` from a 32-byte EVM word encoded as a JSON byte array.
232pub fn read_evm_u256_entry(value: &Value) -> U256 {
233    let result = value_to_vec_u8(value);
234    U256::from_be_slice(&result)
235}
236
237/// Reads an `Address` from the last 20 bytes of a 32-byte EVM word encoded as a JSON byte array.
238pub fn read_evm_address_entry(value: &Value) -> Address {
239    let vec = value_to_vec_u8(value);
240    let mut arr = [0_u8; 20];
241    arr.copy_from_slice(&vec[12..]);
242    Address::from_slice(&arr)
243}