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(path: &Path, file_name: &str) -> anyhow::Result<()> {
21    let mut source = File::create(path).unwrap();
22    writeln!(
23        source,
24        r#"
25{{
26  "language": "Solidity",
27  "sources": {{
28    "{file_name}": {{
29      "urls": ["./{file_name}"]
30    }}
31  }},
32  "settings": {{
33    "viaIR": true,
34    "outputSelection": {{
35      "*": {{
36        "*": ["evm.bytecode"]
37      }}
38    }}
39  }}
40}}
41"#
42    )?;
43    Ok(())
44}
45
46fn get_bytecode_path(path: &Path, file_name: &str, contract_name: &str) -> anyhow::Result<Vec<u8>> {
47    let config_path = path.join("config.json");
48    write_compilation_json(&config_path, file_name)?;
49    let config_file = File::open(config_path)?;
50
51    let output_path = path.join("result.json");
52    let output_file = File::create(output_path.clone())?;
53
54    let status = Command::new("solc")
55        .current_dir(path)
56        .arg("--standard-json")
57        .stdin(Stdio::from(config_file))
58        .stdout(Stdio::from(output_file))
59        .status()?;
60    assert!(status.success());
61
62    let contents = std::fs::read_to_string(output_path)?;
63    let json_data: serde_json::Value = serde_json::from_str(&contents)?;
64    let contracts = json_data
65        .get("contracts")
66        .with_context(|| format!("failed to get contracts in json_data={json_data}"))?;
67    let file_name_contract = contracts
68        .get(file_name)
69        .context("failed to get {file_name}")?;
70    let test_data = file_name_contract
71        .get(contract_name)
72        .with_context(|| format!("failed to get contract_name={contract_name}"))?;
73    let evm_data = test_data
74        .get("evm")
75        .with_context(|| format!("failed to get evm in test_data={test_data}"))?;
76    let bytecode = evm_data
77        .get("bytecode")
78        .with_context(|| format!("failed to get bytecode in evm_data={evm_data}"))?;
79    let object = bytecode
80        .get("object")
81        .with_context(|| format!("failed to get object in bytecode={bytecode}"))?;
82    let object = object.to_string();
83    let object = object.trim_matches(|c| c == '"').to_string();
84    Ok(hex::decode(&object)?)
85}
86
87pub fn compile_solidity_contract(
88    source_code: &str,
89    file_name: &str,
90    contract_name: &str,
91    extra_sources: &[(&str, &str)],
92) -> anyhow::Result<Vec<u8>> {
93    let dir = tempdir().unwrap();
94    let path = dir.path();
95    for (extra_file_name, extra_source_code) in extra_sources {
96        let extra_code_path = path.join(extra_file_name);
97        let mut extra_code_file = File::create(&extra_code_path)?;
98        writeln!(extra_code_file, "{}", extra_source_code)?;
99    }
100    if source_code.contains("Linera.sol") {
101        // The source code seems to import Linera.sol, so we import the relevant files.
102        for (file_name, literal_path) in [
103            ("Linera.sol", LINERA_SOL),
104            ("LineraTypes.sol", LINERA_TYPES_SOL),
105        ] {
106            let test_code_path = path.join(file_name);
107            let mut test_code_file = File::create(&test_code_path)?;
108            writeln!(test_code_file, "{}", literal_path)?;
109        }
110    }
111    if source_code.contains("@openzeppelin") {
112        let _output = Command::new("npm")
113            .args(["install", "@openzeppelin/contracts"])
114            .current_dir(path)
115            .output()?;
116        let _output = Command::new("mv")
117            .args(["node_modules/@openzeppelin", "@openzeppelin"])
118            .current_dir(path)
119            .output()?;
120    }
121    let test_code_path = path.join(file_name);
122    let mut test_code_file = File::create(&test_code_path)?;
123    writeln!(test_code_file, "{}", source_code)?;
124    get_bytecode_path(path, file_name, contract_name)
125}
126
127pub fn get_bytecode(source_code: &str, contract_name: &str) -> anyhow::Result<Vec<u8>> {
128    compile_solidity_contract(source_code, "test_code.sol", contract_name, &[])
129}
130
131pub fn load_solidity_example(path: &str) -> anyhow::Result<Vec<u8>> {
132    let source_code = std::fs::read_to_string(path)?;
133    let contract_name: &str = source_code
134        .lines()
135        .find_map(|line| line.trim_start().strip_prefix("contract "))
136        .ok_or_else(|| anyhow::anyhow!("Not matching"))?;
137    let contract_name: &str = contract_name
138        .split_whitespace()
139        .next()
140        .ok_or_else(|| anyhow::anyhow!("No space found after the contract name"))?;
141    tracing::info!("load_solidity_example, contract_name={contract_name}");
142    get_bytecode(&source_code, contract_name)
143}
144
145pub fn load_solidity_example_by_name(path: &str, contract_name: &str) -> anyhow::Result<Vec<u8>> {
146    let source_code = std::fs::read_to_string(path)?;
147    get_bytecode(&source_code, contract_name)
148}
149
150pub fn temporary_write_evm_module(module: &[u8]) -> anyhow::Result<(PathBuf, TempDir)> {
151    let dir = tempfile::tempdir()?;
152    let path = dir.path();
153    let app_file = "app.json";
154    let app_path = path.join(app_file);
155    {
156        std::fs::write(app_path.clone(), module)?;
157    }
158    let evm_contract = app_path.to_path_buf();
159    Ok((evm_contract, dir))
160}
161
162pub fn get_evm_contract_path(path: &str) -> anyhow::Result<(PathBuf, TempDir)> {
163    let module = load_solidity_example(path)?;
164    temporary_write_evm_module(&module)
165}
166
167pub fn value_to_vec_u8(value: &Value) -> Vec<u8> {
168    let mut vec: Vec<u8> = Vec::new();
169    for val in value.as_array().unwrap() {
170        let val = val.as_u64().unwrap();
171        let val = val as u8;
172        vec.push(val);
173    }
174    vec
175}
176
177pub fn read_evm_u64_entry(value: &Value) -> u64 {
178    let vec = value_to_vec_u8(value);
179    let mut arr = [0_u8; 8];
180    arr.copy_from_slice(&vec[24..]);
181    u64::from_be_bytes(arr)
182}
183
184pub fn read_evm_u256_entry(value: &Value) -> U256 {
185    let result = value_to_vec_u8(value);
186    U256::from_be_slice(&result)
187}
188
189pub fn read_evm_address_entry(value: &Value) -> Address {
190    let vec = value_to_vec_u8(value);
191    let mut arr = [0_u8; 20];
192    arr.copy_from_slice(&vec[12..]);
193    Address::from_slice(&arr)
194}