From e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Rodr=C3=ADguez?= Date: Mon, 14 Oct 2024 14:40:37 -0300 Subject: [PATCH] feat: add formal ways to detect zksync solc throughout the compilation flow (#30) --- crates/artifacts/zksolc/src/lib.rs | 5 + .../compilers/src/compilers/zksolc/input.rs | 2 +- crates/compilers/src/compilers/zksolc/mod.rs | 216 +++++++++++------- .../src/zksync/compile/output/mod.rs | 8 +- .../compilers/src/zksync/compile/project.rs | 25 +- crates/compilers/tests/zksync.rs | 3 +- 6 files changed, 163 insertions(+), 96 deletions(-) diff --git a/crates/artifacts/zksolc/src/lib.rs b/crates/artifacts/zksolc/src/lib.rs index 00c74fc8..504c2656 100644 --- a/crates/artifacts/zksolc/src/lib.rs +++ b/crates/artifacts/zksolc/src/lib.rs @@ -2,6 +2,7 @@ use foundry_compilers_artifacts_solc::{ CompactContractRef, FileToContractsMap, SourceFile, SourceFiles, }; +use semver::Version; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashSet}, @@ -36,6 +37,10 @@ pub struct CompilerOutput { /// The `zksolc` compiler version. #[serde(skip_serializing_if = "Option::is_none")] pub zk_version: Option, + /// The ZKsync solc compiler version (if it was used). This field is + /// inserted by this crate and not an actual part of the compiler output + #[serde(skip_serializing_if = "Option::is_none")] + pub zksync_solc_version: Option, } impl CompilerOutput { diff --git a/crates/compilers/src/compilers/zksolc/input.rs b/crates/compilers/src/compilers/zksolc/input.rs index eb65cc35..0d83cd9c 100644 --- a/crates/compilers/src/compilers/zksolc/input.rs +++ b/crates/compilers/src/compilers/zksolc/input.rs @@ -63,7 +63,7 @@ impl CompilerInput for ZkSolcVersionedInput { } } -/// Input type `solc` expects. +/// Input type `zksolc` expects. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ZkSolcInput { pub language: SolcLanguage, diff --git a/crates/compilers/src/compilers/zksolc/mod.rs b/crates/compilers/src/compilers/zksolc/mod.rs index 7c8e2c8d..153b76da 100644 --- a/crates/compilers/src/compilers/zksolc/mod.rs +++ b/crates/compilers/src/compilers/zksolc/mod.rs @@ -91,8 +91,9 @@ pub struct ZkSolcCompiler { #[cfg(feature = "project-util")] impl Default for ZkSolcCompiler { fn default() -> Self { - let zksolc = ZkSolc::new_from_version(&ZKSOLC_VERSION).expect("Could not install zksolc"); - Self { zksolc: zksolc.zksolc, solc: Default::default() } + let zksolc = + ZkSolc::get_path_for_version(&ZKSOLC_VERSION).expect("Could not install zksolc"); + Self { zksolc, solc: Default::default() } } } @@ -110,7 +111,7 @@ impl Compiler for ZkSolcCompiler { // This method cannot be implemented until CompilerOutput is decoupled from // evm Contract panic!( - "`Compiler::create` not supported for `ZkSolcCompiler`, should call `zksync_compile`." + "`Compiler::compile` not supported for `ZkSolcCompiler`, should call ZkSolc::compile()" ); } @@ -149,11 +150,9 @@ impl Compiler for ZkSolcCompiler { } impl ZkSolcCompiler { - pub fn zksync_compile(&self, input: &ZkSolcVersionedInput) -> Result { - let mut zksolc = ZkSolc::new(self.zksolc.clone()); - - match &self.solc { - SolcCompiler::Specific(solc) => zksolc.solc = Some(solc.solc.clone()), + pub fn zksolc(&self, input: &ZkSolcVersionedInput) -> Result { + let solc = match &self.solc { + SolcCompiler::Specific(solc) => Some(solc.solc.clone()), SolcCompiler::AutoDetect => { #[cfg(test)] crate::take_solc_installer_lock!(_lock); @@ -165,26 +164,71 @@ impl ZkSolcCompiler { let maybe_solc = ZkSolc::find_solc_installed_version(&solc_version_without_metadata)?; if let Some(solc) = maybe_solc { - zksolc.solc = Some(solc); + Some(solc) } else { #[cfg(feature = "async")] { let installed_solc_path = ZkSolc::solc_blocking_install(&solc_version_without_metadata)?; - zksolc.solc = Some(installed_solc_path); + Some(installed_solc_path) } } } - } + }; + + let mut zksolc = ZkSolc::new(self.zksolc.clone(), solc)?; zksolc.base_path.clone_from(&input.cli_settings.base_path); zksolc.allow_paths.clone_from(&input.cli_settings.allow_paths); zksolc.include_paths.clone_from(&input.cli_settings.include_paths); - zksolc.compile(&input.input) + Ok(zksolc) } } +/// Version metadata. Will include `zksync_version` if compiler is zksync solc. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct SolcVersionInfo { + /// The solc compiler version (e.g: 0.8.20) + pub version: Version, + /// The full zksync solc compiler version (e.g: 0.8.20-1.0.1) + pub zksync_version: Option, +} + +/// Given a solc path, get both the solc semver and optional zkSync version. +pub fn get_solc_version_info(path: &Path) -> Result { + let mut cmd = Command::new(path); + cmd.arg("--version").stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped()); + debug!(?cmd, "getting Solc versions"); + + let output = cmd.output().map_err(|e| SolcError::io(e, path))?; + trace!(?output); + + if !output.status.success() { + return Err(SolcError::solc_output(&output)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect(); + + // Get solc version from second line + let version = lines.get(1).ok_or_else(|| SolcError::msg("Version not found in Solc output"))?; + let version = + Version::from_str(&version.trim_start_matches("Version: ").replace(".g++", ".gcc"))?; + + // Check for ZKsync version in the last line + let zksync_version = lines.last().and_then(|line| { + if line.starts_with("ZKsync") { + let version_str = line.trim_start_matches("ZKsync:").trim(); + Version::parse(version_str).ok() + } else { + None + } + }); + + Ok(SolcVersionInfo { version, zksync_version }) +} + /// Abstraction over `zksolc` command line utility /// /// Supports sync and async functions. @@ -204,38 +248,45 @@ pub struct ZkSolc { pub include_paths: BTreeSet, /// Value for --solc arg pub solc: Option, -} - -impl Default for ZkSolc { - fn default() -> Self { - if let Ok(zksolc) = std::env::var("ZKSOLC_PATH") { - return Self::new(zksolc); - } - - Self::new(ZKSOLC) - } + /// Version data for solc + pub solc_version_info: SolcVersionInfo, } impl ZkSolc { /// A new instance which points to `zksolc` - pub fn new(path: impl Into) -> Self { - Self { - zksolc: path.into(), + pub fn new(path: PathBuf, solc: Option) -> Result { + let default_solc_path = PathBuf::from("solc"); + let solc_path = solc.as_ref().unwrap_or(&default_solc_path); + let solc_version_info = get_solc_version_info(solc_path)?; + Ok(Self { + zksolc: path, base_path: None, allow_paths: Default::default(), include_paths: Default::default(), - solc: None, - } + solc, + solc_version_info, + }) } - pub fn new_from_version(version: &Version) -> Result { + pub fn get_path_for_version(version: &Version) -> Result { let maybe_zksolc = Self::find_installed_version(version)?; - if let Some(zksolc) = maybe_zksolc { - Ok(zksolc) - } else { - Self::blocking_install(version) - } + let path = + if let Some(zksolc) = maybe_zksolc { zksolc } else { Self::blocking_install(version)? }; + + Ok(path) + } + + /// Invokes `zksolc --version` and parses the output as a SemVer [`Version`]. + pub fn get_version_for_path(path: &Path) -> Result { + let mut cmd = Command::new(path); + cmd.arg("--version").stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped()); + debug!(?cmd, "getting ZkSolc version"); + let output = cmd.output().map_err(map_io_err(path))?; + trace!(?output); + let version = version_from_output(output)?; + debug!(%version); + Ok(version) } /// Sets zksolc's base path @@ -246,11 +297,16 @@ impl ZkSolc { /// Compiles with `--standard-json` and deserializes the output as [`CompilerOutput`]. pub fn compile(&self, input: &ZkSolcInput) -> Result { + // If solc is zksync solc, override the returned version to put the complete zksolc one let output = self.compile_output(input)?; // Only run UTF-8 validation once. let output = std::str::from_utf8(&output).map_err(|_| SolcError::InvalidUtf8)?; - Ok(serde_json::from_str(output)?) + let mut compiler_output: CompilerOutput = serde_json::from_str(output)?; + // Add zksync version so that there's some way to identify if zksync solc was used + // by looking at build info + compiler_output.zksync_solc_version = self.solc_version_info.zksync_version.clone(); + Ok(compiler_output) } pub fn solc_installed_versions() -> Vec { @@ -323,43 +379,19 @@ impl ZkSolc { trace!(input=%serde_json::to_string(input).unwrap_or_else(|e| e.to_string())); debug!(?cmd, "compiling"); - let mut child = cmd.spawn().map_err(self.map_io_err())?; + let mut child = cmd.spawn().map_err(map_io_err(&self.zksolc))?; debug!("spawned"); let stdin = child.stdin.as_mut().unwrap(); serde_json::to_writer(stdin, input)?; debug!("wrote JSON input to stdin"); - let output = child.wait_with_output().map_err(self.map_io_err())?; + let output = child.wait_with_output().map_err(map_io_err(&self.zksolc))?; debug!(%output.status, output.stderr = ?String::from_utf8_lossy(&output.stderr), "finished"); compile_output(output) } - /// Invokes `zksolc --version` and parses the output as a SemVer [`Version`], stripping the - /// pre-release and build metadata. - pub fn version_short(&self) -> Result { - let version = self.version()?; - Ok(Version::new(version.major, version.minor, version.patch)) - } - - /// Invokes `zksolc --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] - pub fn version(&self) -> Result { - let mut cmd = Command::new(&self.zksolc); - cmd.arg("--version").stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped()); - debug!(?cmd, "getting ZkSolc version"); - let output = cmd.output().map_err(self.map_io_err())?; - trace!(?output); - let version = version_from_output(output)?; - debug!(%version); - Ok(version) - } - - fn map_io_err(&self) -> impl FnOnce(std::io::Error) -> SolcError + '_ { - move |err| SolcError::io(err, &self.zksolc) - } - fn compilers_dir() -> Result { let mut compilers_dir = dirs::home_dir() .ok_or(SolcError::msg("Could not build SolcManager - homedir not found"))?; @@ -384,7 +416,7 @@ impl ZkSolc { /// Install zksolc version and block the thread #[cfg(feature = "async")] - pub fn blocking_install(version: &Version) -> Result { + pub fn blocking_install(version: &Version) -> Result { let os = get_operating_system()?; let compiler_prefix = os.get_zksolc_prefix(); let download_url = if version.pre.is_empty() { @@ -415,7 +447,7 @@ impl ZkSolc { match install { Ok(path) => { //crate::report::solc_installation_success(version); - Ok(Self::new(path)) + Ok(path) } Err(err) => { //crate::report::solc_installation_error(version, &err.to_string()); @@ -445,13 +477,13 @@ impl ZkSolc { compiler_blocking_install(solc_path, lock_path, &download_url, &label) } - pub fn find_installed_version(version: &Version) -> Result> { + pub fn find_installed_version(version: &Version) -> Result> { let zksolc = Self::compiler_path(version)?; if !zksolc.is_file() { return Ok(None); } - Ok(Some(Self::new(zksolc))) + Ok(Some(zksolc)) } pub fn find_solc_installed_version(version_str: &str) -> Result> { @@ -464,6 +496,10 @@ impl ZkSolc { } } +fn map_io_err(zksolc_path: &Path) -> impl FnOnce(std::io::Error) -> SolcError + '_ { + move |err| SolcError::io(err, zksolc_path) +} + fn compile_output(output: Output) -> Result> { if output.status.success() { Ok(output.stdout) @@ -499,12 +535,6 @@ impl AsRef for ZkSolc { } } -impl> From for ZkSolc { - fn from(zksolc: T) -> Self { - Self::new(zksolc.into()) - } -} - impl CompilationError for Error { fn is_warning(&self) -> bool { self.severity.is_warning() @@ -630,28 +660,56 @@ fn lock_file_path(compiler: &str, version: &str) -> PathBuf { #[cfg(test)] mod tests { + use similar_asserts::assert_eq; + + use crate::solc::Solc; + use super::*; fn zksolc() -> ZkSolc { - let mut zksolc = ZkSolc::new_from_version(&ZKSOLC_VERSION).unwrap(); + let zksolc_path = ZkSolc::get_path_for_version(&ZKSOLC_VERSION).unwrap(); let solc_version = "0.8.27"; crate::take_solc_installer_lock!(_lock); let maybe_solc = ZkSolc::find_solc_installed_version(solc_version).unwrap(); - if let Some(solc) = maybe_solc { - zksolc.solc = Some(solc); + let solc_path = if let Some(solc) = maybe_solc { + solc } else { - { - let installed_solc_path = ZkSolc::solc_blocking_install(solc_version).unwrap(); - zksolc.solc = Some(installed_solc_path); - } + ZkSolc::solc_blocking_install(solc_version).unwrap() + }; + ZkSolc::new(zksolc_path, Some(solc_path)).unwrap() + } + + fn vanilla_solc() -> Solc { + if let Some(solc) = Solc::find_svm_installed_version(&Version::new(0, 8, 18)).unwrap() { + solc + } else { + Solc::blocking_install(&Version::new(0, 8, 18)).unwrap() } - zksolc } #[test] fn zksolc_version_works() { - zksolc().version().unwrap(); + ZkSolc::get_version_for_path(&zksolc().zksolc).unwrap(); + } + + #[test] + fn get_solc_type_and_version_works_for_zksync_solc() { + let zksolc = zksolc(); + let solc = zksolc.solc.unwrap(); + let solc_v = get_solc_version_info(&solc).unwrap(); + let zksync_v = solc_v.zksync_version.unwrap(); + let prerelease = Version::parse(zksync_v.pre.as_str()).unwrap(); + assert_eq!(solc_v.version.minor, 8); + assert_eq!(prerelease, ZKSYNC_SOLC_RELEASE); + } + + #[test] + fn get_solc_type_and_version_works_for_vanilla_solc() { + let solc = vanilla_solc(); + let solc_v = get_solc_version_info(&solc.solc).unwrap(); + assert_eq!(solc_v.version.minor, 8); + assert!(solc_v.zksync_version.is_none()); } #[test] diff --git a/crates/compilers/src/zksync/compile/output/mod.rs b/crates/compilers/src/zksync/compile/output/mod.rs index e863fac3..74d502e5 100644 --- a/crates/compilers/src/zksync/compile/output/mod.rs +++ b/crates/compilers/src/zksync/compile/output/mod.rs @@ -258,11 +258,7 @@ impl AggregatedCompilerOutput { /// Checks if there are any compiler warnings that are not ignored by the specified error codes /// and file paths. - pub fn has_warning<'a>( - &self, - ignored_error_codes: &[u64], - ignored_file_paths: &[PathBuf], - ) -> bool { + pub fn has_warning(&self, ignored_error_codes: &[u64], ignored_file_paths: &[PathBuf]) -> bool { self.errors .iter() .any(|error| !self.should_ignore(ignored_error_codes, ignored_file_paths, error)) @@ -532,7 +528,7 @@ impl<'a> OutputDiagnostics<'a> { /// Returns true if there is at least one warning pub fn has_warning(&self) -> bool { - self.compiler_output.has_warning(&self.ignored_error_codes, &self.ignored_file_paths) + self.compiler_output.has_warning(self.ignored_error_codes, self.ignored_file_paths) } /// Returns true if the contract is a expected to be a test diff --git a/crates/compilers/src/zksync/compile/project.rs b/crates/compilers/src/zksync/compile/project.rs index 6eee6853..d44f2fb4 100644 --- a/crates/compilers/src/zksync/compile/project.rs +++ b/crates/compilers/src/zksync/compile/project.rs @@ -282,7 +282,7 @@ impl CompilerSources { } } - /// Compiles all the files with `Solc` + /// Compiles all the files with `ZkSolc` fn compile( self, cache: &mut ArtifactsCache<'_, ZkArtifactOutput, ZkSolcCompiler>, @@ -370,19 +370,26 @@ impl CompilerSources { /// Compiles the input set sequentially and returns an aggregated set of the solc `CompilerOutput`s fn compile_sequential( - zksolc: &ZkSolcCompiler, + zksolc_compiler: &ZkSolcCompiler, jobs: Vec<(ZkSolcVersionedInput, Vec)>, ) -> Result)>> { jobs.into_iter() .map(|(input, actually_dirty)| { + let zksolc = zksolc_compiler.zksolc(&input)?; + + let (compiler_name, version) = + if let Some(zk_version) = zksolc.solc_version_info.zksync_version.as_ref() { + ("zksolc and ZKsync solc".to_string(), zk_version.clone()) + } else { + (input.compiler_name().to_string(), input.version().clone()) + }; + let start = Instant::now(); - report::compiler_spawn( - &input.compiler_name(), - input.version(), - actually_dirty.as_slice(), - ); - let output = zksolc.zksync_compile(&input)?; - report::compiler_success(&input.compiler_name(), input.version(), &start.elapsed()); + report::compiler_spawn(&compiler_name, &version, actually_dirty.as_slice()); + + let output = zksolc.compile(&input.input)?; + + report::compiler_success(&compiler_name, &version, &start.elapsed()); Ok((input, output, actually_dirty)) }) diff --git a/crates/compilers/tests/zksync.rs b/crates/compilers/tests/zksync.rs index b5b4b648..efacf653 100644 --- a/crates/compilers/tests/zksync.rs +++ b/crates/compilers/tests/zksync.rs @@ -241,11 +241,12 @@ contract B { } let mut build_info_count = 0; for entry in fs::read_dir(info_dir).unwrap() { - let _info = + let info = BuildInfo::::read( &entry.unwrap().path(), ) .unwrap(); + assert!(info.output.zksync_solc_version.is_some()); build_info_count += 1; } assert_eq!(build_info_count, 1);