Repository: gakonst/ethers-rs
Pull request #1043 (2022-03-16 16:41:17)

feat(solc): support for solc io json output
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
Add support for logging the CompilerInput and output.
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
Additional type that can be invoked by the existing `Reporter` functions and can be configured using env vars.
Needs forge companion
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/ethers-core/src/types/block.rs b/ethers-core/src/types/block.rs
index 61c266fc..a0ef2b5a 100644
--- a/ethers-core/src/types/block.rs
+++ b/ethers-core/src/types/block.rs
@@ -1,5 +1,7 @@
 // Taken from https://github.com/tomusdrw/rust-web3/blob/master/src/types/block.rs
 use crate::types::{Address, Bloom, Bytes, H256, U256, U64};
+#[cfg(not(feature = "celo"))]
+use core::cmp::Ordering;
 use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
 use std::str::FromStr;
 
@@ -89,6 +91,52 @@ pub struct Block<TX> {
     pub epoch_snark_data: Option<EpochSnarkData>,
 }
 
+// ref https://eips.ethereum.org/EIPS/eip-1559
+#[cfg(not(feature = "celo"))]
+pub const ELASTICITY_MULTIPLIER: U256 = U256([2u64, 0, 0, 0]);
+// max base fee delta is 12.5%
+#[cfg(not(feature = "celo"))]
+pub const BASE_FEE_MAX_CHANGE_DENOMINATOR: U256 = U256([8u64, 0, 0, 0]);
+
+impl<TX> Block<TX> {
+    /// The target gas usage as per EIP-1559
+    #[cfg(not(feature = "celo"))]
+    pub fn gas_target(&self) -> U256 {
+        self.gas_limit / ELASTICITY_MULTIPLIER
+    }
+
+    /// The next block's base fee, it is a function of parent block's base fee and gas usage.
+    /// Reference: https://eips.ethereum.org/EIPS/eip-1559
+    #[cfg(not(feature = "celo"))]
+    pub fn next_block_base_fee(&self) -> Option<U256> {
+        let target_usage = self.gas_target();
+        let base_fee_per_gas = self.base_fee_per_gas?;
+
+        match self.gas_used.cmp(&target_usage) {
+            Ordering::Greater => {
+                let gas_used_delta = self.gas_used - self.gas_target();
+                let base_fee_per_gas_delta = U256::max(
+                    base_fee_per_gas * gas_used_delta /
+                        target_usage /
+                        BASE_FEE_MAX_CHANGE_DENOMINATOR,
+                    U256::from(1u32),
+                );
+                let expected_base_fee_per_gas = base_fee_per_gas + base_fee_per_gas_delta;
+                Some(expected_base_fee_per_gas)
+            }
+            Ordering::Less => {
+                let gas_used_delta = self.gas_target() - self.gas_used;
+                let base_fee_per_gas_delta = base_fee_per_gas * gas_used_delta /
+                    target_usage /
+                    BASE_FEE_MAX_CHANGE_DENOMINATOR;
+                let expected_base_fee_per_gas = base_fee_per_gas - base_fee_per_gas_delta;
+                Some(expected_base_fee_per_gas)
+            }
+            Ordering::Equal => self.base_fee_per_gas,
+        }
+    }
+}
+
 #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
 #[cfg(feature = "celo")]
 /// Commit-reveal data for generating randomness in the
@@ -279,6 +327,33 @@ mod tests {
         let block: Block<()> = serde_json::from_value(json).unwrap();
         assert_eq!(block.base_fee_per_gas, Some(U256::from(7)));
     }
+
+    #[test]
+    fn test_next_block_base_fee() {
+        // https://etherscan.io/block/14402566
+        let mut block_14402566 = Block::<TxHash>::default();
+        block_14402566.number = Some(U64::from(14402566u64));
+        block_14402566.base_fee_per_gas = Some(U256::from(36_803_013_756u128));
+        block_14402566.gas_limit = U256::from(30_087_887u128);
+        block_14402566.gas_used = U256::from(2_023_848u128);
+
+        assert_eq!(block_14402566.base_fee_per_gas, Some(U256::from(36_803_013_756u128)));
+        assert_eq!(block_14402566.gas_target(), U256::from(15_043_943u128));
+        // next block decreasing base fee https://etherscan.io/block/14402567
+        assert_eq!(block_14402566.next_block_base_fee(), Some(U256::from(32_821_521_542u128)));
+
+        // https://etherscan.io/block/14402712
+        let mut block_14402712 = Block::<TxHash>::default();
+        block_14402712.number = Some(U64::from(14402712u64));
+        block_14402712.base_fee_per_gas = Some(U256::from(24_870_031_149u128));
+        block_14402712.gas_limit = U256::from(30_000_000u128);
+        block_14402712.gas_used = U256::from(29_999_374u128);
+
+        assert_eq!(block_14402712.base_fee_per_gas, Some(U256::from(24_870_031_149u128)));
+        assert_eq!(block_14402712.gas_target(), U256::from(15_000_000u128));
+        // next block increasing base fee https://etherscan.io/block/14402713
+        assert_eq!(block_14402712.next_block_base_fee(), Some(U256::from(27_978_655_303u128)));
+    }
 }
 
 #[cfg(test)]
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index ef9d230d..79b82dd8 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -158,6 +158,10 @@ pub struct Settings {
         skip_serializing_if = "Option::is_none"
     )]
     pub evm_version: Option<EvmVersion>,
+    /// Change compilation pipeline to go through the Yul intermediate representation. This is
+    /// false by default.
+    #[serde(rename = "viaIR", default, skip_serializing_if = "Option::is_none")]
+    pub via_ir: Option<bool>,
     #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
     pub libraries: BTreeMap<String, BTreeMap<String, String>>,
 }
@@ -240,6 +244,19 @@ impl Settings {
             .insert(key.into(), values.into_iter().map(|s| s.to_string()).collect());
     }
 
+    /// Sets the ``viaIR` valu
+    #[must_use]
+    pub fn set_via_ir(mut self, via_ir: bool) -> Self {
+        self.via_ir = Some(via_ir);
+        self
+    }
+
+    /// Enables `viaIR`
+    #[must_use]
+    pub fn with_via_ir(self) -> Self {
+        self.set_via_ir(true)
+    }
+
     /// Adds `ast` to output
     #[must_use]
     pub fn with_ast(mut self) -> Self {
@@ -258,6 +275,7 @@ impl Default for Settings {
             metadata: None,
             output_selection: OutputSelection::default_output_selection(),
             evm_version: Some(EvmVersion::default()),
+            via_ir: None,
             libraries: Default::default(),
             remappings: Default::default(),
         }
diff --git a/ethers-solc/src/project_util/mock.rs b/ethers-solc/src/project_util/mock.rs
index 78fb81a9..8576a520 100644
--- a/ethers-solc/src/project_util/mock.rs
+++ b/ethers-solc/src/project_util/mock.rs
@@ -12,7 +12,7 @@ use rand::{
 };
 use serde::{Deserialize, Serialize};
 use std::{
-    collections::{BTreeSet, HashMap},
+    collections::{BTreeSet, HashMap, HashSet, VecDeque},
     path::{Path, PathBuf},
 };
 
@@ -25,6 +25,13 @@ pub struct MockProjectSkeleton {
     pub libraries: Vec<MockLib>,
 }
 
+impl MockProjectSkeleton {
+    /// Returns a list of file ids the given file id imports.
+    pub fn imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
+        self.files[from].imports.iter().map(|i| i.file_id())
+    }
+}
+
 /// Represents a virtual project
 #[derive(Serialize)]
 pub struct MockProjectGenerator {
@@ -272,11 +279,27 @@ impl MockProjectGenerator {
         }
     }
 
+    /// Returns the file for the given id
+    pub fn get_file(&self, id: usize) -> &MockFile {
+        &self.inner.files[id]
+    }
+
     /// All file ids
     pub fn file_ids(&self) -> impl Iterator<Item = usize> + '_ {
         self.inner.files.iter().map(|f| f.id)
     }
 
+    /// Returns an iterator over all file ids that are source files or imported by source files
+    ///
+    /// In other words, all files that are relevant in order to compile the project's source files.
+    pub fn used_file_ids(&self) -> impl Iterator<Item = usize> + '_ {
+        let mut file_ids = BTreeSet::new();
+        for file in self.internal_file_ids() {
+            file_ids.extend(NodesIter::new(file, &self.inner))
+        }
+        file_ids.into_iter()
+    }
+
     /// All ids of internal files
     pub fn internal_file_ids(&self) -> impl Iterator<Item = usize> + '_ {
         self.inner.files.iter().filter(|f| !f.is_external()).map(|f| f.id)
@@ -469,6 +492,15 @@ pub enum MockImport {
     External(usize, usize),
 }
 
+impl MockImport {
+    pub fn file_id(&self) -> usize {
+        *match self {
+            MockImport::Internal(id) => id,
+            MockImport::External(_, id) => id,
+        }
+    }
+}
+
 /// Container of a mock lib
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct MockLib {
@@ -552,6 +584,33 @@ impl Default for MockProjectSettings {
     }
 }
 
+/// An iterator over a node and its dependencies
+struct NodesIter<'a> {
+    /// stack of nodes
+    stack: VecDeque<usize>,
+    visited: HashSet<usize>,
+    skeleton: &'a MockProjectSkeleton,
+}
+
+impl<'a> NodesIter<'a> {
+    fn new(start: usize, skeleton: &'a MockProjectSkeleton) -> Self {
+        Self { stack: VecDeque::from([start]), visited: HashSet::new(), skeleton }
+    }
+}
+
+impl<'a> Iterator for NodesIter<'a> {
+    type Item = usize;
+    fn next(&mut self) -> Option<Self::Item> {
+        let file = self.stack.pop_front()?;
+
+        if self.visited.insert(file) {
+            // push the file's direct imports to the stack if we haven't visited it already
+            self.stack.extend(self.skeleton.imported_nodes(file));
+        }
+        Some(file)
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/ethers-solc/src/report/compiler.rs b/ethers-solc/src/report/compiler.rs
new file mode 100644
index 00000000..739c98cf
--- /dev/null
+++ b/ethers-solc/src/report/compiler.rs
@@ -0,0 +1,189 @@
+//! Additional logging [CompilerInput] and [CompilerOutput]
+//!
+//! Useful for debugging purposes.
+//! As solc compiler input and output can become quite large (in the tens of MB) we still want a way
+//! to get this info when debugging an issue. Most convenient way to look at these object is as a
+//! separate json file
+
+use crate::{CompilerInput, CompilerOutput};
+use std::{env, path::PathBuf, str::FromStr};
+
+/// Debug Helper type that can be used to write the [Solc] [CompilerInput] and [CompilerOutput] to
+/// disk if configured.
+///
+/// # Example
+///
+/// If `ETHERS_SOLC_LOG=in=in.json,out=out.json` is then the reporter will be configured to write
+/// the compiler input as pretty formatted json to `in.json` and the compiler output to `out.json`
+///
+/// ```no_run
+/// use ethers_solc::report::SolcCompilerIoReporter;
+/// std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json");
+/// let rep = SolcCompilerIoReporter::from_default_env();
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct SolcCompilerIoReporter {
+    /// where to write the output to, `None` if not enabled
+    target: Option<Target>,
+}
+
+impl SolcCompilerIoReporter {
+    /// Returns a new `SolcCompilerIOLayer` from the fields in the given string,
+    /// ignoring any that are invalid.
+    pub fn new(value: impl AsRef<str>) -> Self {
+        Self { target: Some(value.as_ref().parse().unwrap_or_default()) }
+    }
+
+    /// `ETHERS_SOLC_LOG` is the default environment variable used by
+    /// [`SolcCompilerIOLayer::from_default_env`]
+    ///
+    /// [`SolcCompilerIOLayer::from_default_env`]: #method.from_default_env
+    pub const DEFAULT_ENV: &'static str = "ETHERS_SOLC_LOG";
+
+    /// Returns a new `SolcCompilerIOLayer` from the value of the `ETHERS_SOLC_LOG` environment
+    /// variable, ignoring any invalid filter directives.
+    pub fn from_default_env() -> Self {
+        Self::from_env(Self::DEFAULT_ENV)
+    }
+
+    /// Returns a new `SolcCompilerIOLayer` from the value of the given environment
+    /// variable, ignoring any invalid filter directives.
+    pub fn from_env<A: AsRef<str>>(env: A) -> Self {
+        env::var(env.as_ref()).map(Self::new).unwrap_or_default()
+    }
+
+    /// Callback to write the input to disk if target is set
+    pub fn log_compiler_input(&self, input: &CompilerInput) {
+        if let Some(ref target) = self.target {
+            target.write_input(input)
+        }
+    }
+
+    /// Callback to write the input to disk if target is set
+    pub fn log_compiler_output(&self, output: &CompilerOutput) {
+        if let Some(ref target) = self.target {
+            target.write_output(output)
+        }
+    }
+}
+
+impl<S> From<S> for SolcCompilerIoReporter
+where
+    S: AsRef<str>,
+{
+    fn from(s: S) -> Self {
+        Self::new(s)
+    }
+}
+
+/// Represents the `in=<path>,out=<path>` value
+#[derive(Debug, Clone, Eq, PartialEq)]
+struct Target {
+    /// path where the compiler input file should be written to
+    dest_input: PathBuf,
+    /// path where the compiler output file should be written to
+    dest_output: PathBuf,
+}
+
+impl Target {
+    fn write_input(&self, input: &CompilerInput) {
+        tracing::trace!("logging compiler input to {}", self.dest_input.display());
+        match serde_json::to_string_pretty(input) {
+            Ok(json) => {
+                if let Err(err) = std::fs::write(&self.dest_input, json) {
+                    tracing::error!("Failed to write compiler input: {}", err)
+                }
+            }
+            Err(err) => {
+                tracing::error!("Failed to serialize compiler input: {}", err)
+            }
+        }
+    }
+
+    fn write_output(&self, output: &CompilerOutput) {
+        tracing::trace!("logging compiler output to {}", self.dest_output.display());
+        match serde_json::to_string_pretty(output) {
+            Ok(json) => {
+                if let Err(err) = std::fs::write(&self.dest_output, json) {
+                    tracing::error!("Failed to write compiler output: {}", err)
+                }
+            }
+            Err(err) => {
+                tracing::error!("Failed to serialize compiler output: {}", err)
+            }
+        }
+    }
+}
+
+impl Default for Target {
+    fn default() -> Self {
+        Self {
+            dest_input: "compiler-input.json".into(),
+            dest_output: "compiler-output.json".into(),
+        }
+    }
+}
+
+impl FromStr for Target {
+    type Err = Box<dyn std::error::Error + Send + Sync>;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut dest_input = None;
+        let mut dest_output = None;
+        for part in s.split(',') {
+            let (name, val) =
+                part.split_once('=').ok_or_else(|| BadName { name: part.to_string() })?;
+            match name {
+                "i" | "in" | "input" | "compilerinput" => {
+                    dest_input = Some(PathBuf::from(val));
+                }
+                "o" | "out" | "output" | "compileroutput" => {
+                    dest_output = Some(PathBuf::from(val));
+                }
+                _ => return Err(BadName { name: part.to_string() }.into()),
+            };
+        }
+
+        Ok(Self {
+            dest_input: dest_input.unwrap_or_else(|| "compiler-input.json".into()),
+            dest_output: dest_output.unwrap_or_else(|| "compiler-output.json".into()),
+        })
+    }
+}
+
+/// Indicates that a field name specified in the env value was invalid.
+#[derive(Clone, Debug, thiserror::Error)]
+#[error("{}", self.name)]
+pub struct BadName {
+    name: String,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn can_parse_target() {
+        let target: Target = "in=in.json,out=out.json".parse().unwrap();
+        assert_eq!(target, Target { dest_input: "in.json".into(), dest_output: "out.json".into() });
+
+        let target: Target = "in=in.json".parse().unwrap();
+        assert_eq!(target, Target { dest_input: "in.json".into(), ..Default::default() });
+
+        let target: Target = "out=out.json".parse().unwrap();
+        assert_eq!(target, Target { dest_output: "out.json".into(), ..Default::default() });
+    }
+
+    #[test]
+    fn can_init_reporter_from_env() {
+        let rep = SolcCompilerIoReporter::from_default_env();
+        assert!(rep.target.is_none());
+        std::env::set_var("ETHERS_SOLC_LOG", "in=in.json,out=out.json");
+        let rep = SolcCompilerIoReporter::from_default_env();
+        assert!(rep.target.is_some());
+        assert_eq!(
+            rep.target.unwrap(),
+            Target { dest_input: "in.json".into(), dest_output: "out.json".into() }
+        );
+        std::env::remove_var("ETHERS_SOLC_LOG");
+    }
+}
diff --git a/ethers-solc/src/report.rs b/ethers-solc/src/report/mod.rs
similarity index 99%
rename from ethers-solc/src/report.rs
rename to ethers-solc/src/report/mod.rs
index 15413012..518ce6b6 100644
--- a/ethers-solc/src/report.rs
+++ b/ethers-solc/src/report/mod.rs
@@ -29,6 +29,9 @@ use std::{
     },
 };
 
+mod compiler;
+pub use compiler::SolcCompilerIoReporter;
+
 thread_local! {
     static CURRENT_STATE: State = State {
         scoped: RefCell::new(Report::none()),


Test patch:
diff --git a/ethers-solc/tests/mocked.rs b/ethers-solc/tests/mocked.rs
index 4bd717b2..3fdffb97 100644
--- a/ethers-solc/tests/mocked.rs
+++ b/ethers-solc/tests/mocked.rs
@@ -3,7 +3,7 @@
 use ethers_solc::{
     error::Result,
     project_util::{
-        mock::{MockProjectGenerator, MockProjectSettings},
+        mock::{MockProjectGenerator, MockProjectSettings, MockProjectSkeleton},
         TempProject,
     },
 };
@@ -85,7 +85,7 @@ fn can_compile_mocked_modified() {
     run_mock(MockProjectSettings::random(), |project, gen| {
         project.ensure_no_errors_recompile_unchanged()?;
         // modify a random file
-        gen.modify_file(gen.file_ids().count() / 2, project.paths(), DEFAULT_VERSION)?;
+        gen.modify_file(gen.used_file_ids().count() / 2, project.paths(), DEFAULT_VERSION)?;
         project.ensure_changed()?;
         project.artifacts_snapshot()?.assert_artifacts_essentials_present();
         Ok(())
@@ -97,7 +97,7 @@ fn can_compile_mocked_modified_all() {
     run_mock(MockProjectSettings::random(), |project, gen| {
         project.ensure_no_errors_recompile_unchanged()?;
         // modify a random file
-        for id in gen.file_ids() {
+        for id in gen.used_file_ids() {
             gen.modify_file(id, project.paths(), DEFAULT_VERSION)?;
             project.ensure_changed()?;
             project.artifacts_snapshot()?.assert_artifacts_essentials_present();
@@ -105,3 +105,27 @@ fn can_compile_mocked_modified_all() {
         Ok(())
     });
 }
+
+// a test useful to manually debug a serialized skeleton
+#[test]
+fn can_compile_skeleton() {
+    let mut project = TempProject::dapptools().unwrap();
+    let s = r#"{"files":[{"id":0,"name":"SourceFile0","imports":[{"External":[0,1]},{"External":[3,4]}],"lib_id":null,"emit_artifacts":true},{"id":1,"name":"SourceFile1","imports":[],"lib_id":0,"emit_artifacts":true},{"id":2,"name":"SourceFile2","imports":[],"lib_id":1,"emit_artifacts":true},{"id":3,"name":"SourceFile3","imports":[],"lib_id":2,"emit_artifacts":true},{"id":4,"name":"SourceFile4","imports":[],"lib_id":3,"emit_artifacts":true}],"libraries":[{"name":"Lib0","id":0,"offset":1,"num_files":1},{"name":"Lib1","id":1,"offset":2,"num_files":1},{"name":"Lib2","id":2,"offset":3,"num_files":1},{"name":"Lib3","id":3,"offset":4,"num_files":1}]}"#;
+    let gen: MockProjectGenerator = serde_json::from_str::<MockProjectSkeleton>(s).unwrap().into();
+    let remappings = gen.remappings_at(project.root());
+    project.paths_mut().remappings.extend(remappings);
+    project.mock(&gen, DEFAULT_VERSION).unwrap();
+
+    // mattsse: helper to show what's being generated
+    // gen.write_to(&ethers_solc::ProjectPathsConfig::dapptools("./skeleton").unwrap(),
+    // DEFAULT_VERSION).unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(!compiled.is_unchanged());
+    for id in gen.used_file_ids() {
+        gen.modify_file(id, project.paths(), DEFAULT_VERSION).unwrap();
+        project.ensure_changed().unwrap();
+        project.artifacts_snapshot().unwrap().assert_artifacts_essentials_present();
+    }
+}