Skip to content

Commit

Permalink
fix: pass build info as buffer (#780)
Browse files Browse the repository at this point in the history
* chore: pass build info as buffer

* Upgrade to HH 2.22.18 and add patch

* Add changeset

* Fix CI

* Add support for Hardhat v3 build infos

* Use separate HH v2 vs v3 build info arrays

* Change patch to minor

* Fix input/output mismatch detection
  • Loading branch information
agostbiro authored Jan 30, 2025
1 parent f8de18a commit af26624
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 491 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-crews-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/edr": minor
---

Improved provider initialization performance by passing build info as buffer which avoids FFI copy overhead
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion crates/edr_napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,27 @@ export interface ProviderConfig {
/** The network ID of the blockchain */
networkId: bigint
}
/** Tracing config for Solidity stack trace generation. */
export interface TracingConfigWithBuffers {
/**
* Build information to use for decoding contracts. Either a Hardhat v2
* build info file that contains both input and output or a Hardhat v3
* build info file that doesn't contain output and a separate output file.
*/
buildInfos?: Array<Uint8Array> | Array<BuildInfoAndOutput>
/** Whether to ignore contracts whose name starts with "Ignored". */
ignoreContracts?: boolean
}
/**
* Hardhat V3 build info where the compiler output is not part of the build
* info file.
*/
export interface BuildInfoAndOutput {
/** The build info input file */
buildInfo: Uint8Array
/** The build info output file */
output: Uint8Array
}
/** The possible reasons for successful termination of the EVM. */
export const enum SuccessReason {
/** The opcode `STOP` was called */
Expand Down Expand Up @@ -595,7 +616,7 @@ export declare class EdrContext {
/** A JSON-RPC provider for Ethereum. */
export declare class Provider {
/**Constructs a new provider with the provided configuration. */
static withConfig(context: EdrContext, config: ProviderConfig, loggerConfig: LoggerConfig, tracingConfig: any, subscriberCallback: (event: SubscriptionEvent) => void): Promise<Provider>
static withConfig(context: EdrContext, config: ProviderConfig, loggerConfig: LoggerConfig, tracingConfig: TracingConfigWithBuffers, subscriberCallback: (event: SubscriptionEvent) => void): Promise<Provider>
/**Handles a JSON-RPC request and returns a JSON-RPC response. */
handleRequest(jsonRequest: string): Promise<Response>
setCallOverrideCallback(callOverrideCallback: (contract_address: Buffer, data: Buffer) => Promise<CallOverrideResult | undefined>): void
Expand Down
71 changes: 67 additions & 4 deletions crates/edr_napi/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::sync::Arc;
use edr_provider::{time::CurrentTime, InvalidRequestReason};
use edr_rpc_eth::jsonrpc;
use edr_solidity::contract_decoder::ContractDecoder;
use napi::{tokio::runtime, Either, Env, JsFunction, JsObject, Status};
use napi::{
bindgen_prelude::Uint8Array, tokio::runtime, Either, Env, JsFunction, JsObject, Status,
};
use napi_derive::napi;

use self::config::ProviderConfig;
Expand Down Expand Up @@ -37,16 +39,17 @@ impl Provider {
_context: &EdrContext,
config: ProviderConfig,
logger_config: LoggerConfig,
tracing_config: serde_json::Value,
tracing_config: TracingConfigWithBuffers,
#[napi(ts_arg_type = "(event: SubscriptionEvent) => void")] subscriber_callback: JsFunction,
) -> napi::Result<JsObject> {
let runtime = runtime::Handle::current();

let config = edr_provider::ProviderConfig::try_from(config)?;

// TODO https://github.com/NomicFoundation/edr/issues/760
let build_info_config: edr_solidity::contract_decoder::BuildInfoConfig =
serde_json::from_value(tracing_config)?;
let build_info_config =
edr_solidity::artifacts::BuildInfoConfig::parse_from_buffers((&tracing_config).into())
.map_err(|err| napi::Error::from_reason(err.to_string()))?;
let contract_decoder = ContractDecoder::new(&build_info_config)
.map_err(|error| napi::Error::from_reason(error.to_string()))?;
let contract_decoder = Arc::new(contract_decoder);
Expand Down Expand Up @@ -245,6 +248,66 @@ impl Provider {
}
}

/// Tracing config for Solidity stack trace generation.
#[napi(object)]
pub struct TracingConfigWithBuffers {
/// Build information to use for decoding contracts. Either a Hardhat v2
/// build info file that contains both input and output or a Hardhat v3
/// build info file that doesn't contain output and a separate output file.
pub build_infos: Option<Either<Vec<Uint8Array>, Vec<BuildInfoAndOutput>>>,
/// Whether to ignore contracts whose name starts with "Ignored".
pub ignore_contracts: Option<bool>,
}

/// Hardhat V3 build info where the compiler output is not part of the build
/// info file.
#[napi(object)]
pub struct BuildInfoAndOutput {
/// The build info input file
pub build_info: Uint8Array,
/// The build info output file
pub output: Uint8Array,
}

impl<'a> From<&'a BuildInfoAndOutput>
for edr_solidity::artifacts::BuildInfoBufferSeparateOutput<'a>
{
fn from(value: &'a BuildInfoAndOutput) -> Self {
Self {
build_info: value.build_info.as_ref(),
output: value.output.as_ref(),
}
}
}

impl<'a> From<&'a TracingConfigWithBuffers>
for edr_solidity::artifacts::BuildInfoConfigWithBuffers<'a>
{
fn from(value: &'a TracingConfigWithBuffers) -> Self {
use edr_solidity::artifacts::{BuildInfoBufferSeparateOutput, BuildInfoBuffers};

let build_infos = value.build_infos.as_ref().map(|infos| match infos {
Either::A(with_output) => BuildInfoBuffers::WithOutput(
with_output
.iter()
.map(std::convert::AsRef::as_ref)
.collect(),
),
Either::B(separate_output) => BuildInfoBuffers::SeparateInputOutput(
separate_output
.iter()
.map(BuildInfoBufferSeparateOutput::from)
.collect(),
),
});

Self {
build_infos,
ignore_contracts: value.ignore_contracts,
}
}
}

#[derive(Debug)]
struct SolidityTraceData {
trace: Arc<edr_evm::trace::Trace>,
Expand Down
1 change: 1 addition & 0 deletions crates/edr_solidity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ serde_json = { version = "1.0.89", features = ["preserve_order"] }
strum = { version = "0.26.0", features = ["derive"] }
semver = "1.0.23"
thiserror = "1.0.58"
itertools = "0.10.5"

[dev-dependencies]
criterion = "0.5"
Expand Down
10 changes: 4 additions & 6 deletions crates/edr_solidity/benches/contracts_identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use std::{fs, path::PathBuf, time::Duration};

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use edr_solidity::{
artifacts::BuildInfo,
contract_decoder::{BuildInfoConfig, ContractDecoder},
artifacts::{BuildInfoConfig, BuildInfoWithOutput},
contract_decoder::ContractDecoder,
};

const FORGE_STD_ARTIFACTS_DIR: &str = "EDR_FORGE_STD_ARTIFACTS_DIR";
Expand All @@ -33,13 +33,13 @@ fn load_build_info_config() -> anyhow::Result<Option<BuildInfoConfig>> {

if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("json") {
let contents = fs::read(&path)?;
let build_info = serde_json::from_slice::<BuildInfo>(&contents)?;
let build_info = serde_json::from_slice::<BuildInfoWithOutput>(&contents)?;
build_infos.push(build_info);
}
}

Ok(Some(BuildInfoConfig {
build_infos: Some(build_infos),
build_infos,
ignore_contracts: None,
}))
}
Expand All @@ -51,8 +51,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {

let contracts = &build_info_config
.build_infos
.as_ref()
.expect("loaded build info")
.first()
.expect("there is at least one build info")
.output
Expand Down
150 changes: 148 additions & 2 deletions crates/edr_solidity/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,148 @@
use std::collections::HashMap;

use indexmap::IndexMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};

/// A `BuildInfo` is a file that contains all the information of a solc run. It
/// Error in the build info config
#[derive(Debug, thiserror::Error)]
pub enum BuildInfoConfigError {
/// JSON deserialization error
#[error(transparent)]
Json(#[from] serde_json::Error),
/// Invalid semver in the build info
#[error(transparent)]
Semver(#[from] semver::Error),
/// Input output file mismatch
#[error("Input output mismatch. Input id: '{input_id}'. Output id: '{output_id}'")]
InputOutputMismatch { input_id: String, output_id: String },
}

/// Configuration for the [`crate::contract_decoder::ContractDecoder`].
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfoConfig {
/// Build information to use for decoding contracts.
pub build_infos: Vec<BuildInfoWithOutput>,
/// Whether to ignore contracts whose name starts with "Ignored".
pub ignore_contracts: Option<bool>,
}

impl BuildInfoConfig {
/// Parse the config from bytes. This is a performance intensive operation
/// which is why it's not a `TryFrom` implementation.
pub fn parse_from_buffers(
config: BuildInfoConfigWithBuffers<'_>,
) -> Result<Self, BuildInfoConfigError> {
let BuildInfoConfigWithBuffers {
build_infos,
ignore_contracts,
} = config;

let build_infos = build_infos.map_or_else(|| Ok(Vec::default()), |bi| bi.parse())?;

Ok(Self {
build_infos,
ignore_contracts,
})
}
}

/// Configuration for the [`crate::contract_decoder::ContractDecoder`] unparsed
/// build infos.
#[derive(Clone, Debug)]
pub struct BuildInfoConfigWithBuffers<'a> {
/// Build information to use for decoding contracts.
pub build_infos: Option<BuildInfoBuffers<'a>>,
/// Whether to ignore contracts whose name starts with "Ignored".
pub ignore_contracts: Option<bool>,
}

/// Unparsed build infos.
#[derive(Clone, Debug)]
pub enum BuildInfoBuffers<'a> {
/// Deserializes to `BuildInfoWithOutput`.
WithOutput(Vec<&'a [u8]>),
/// Separate build info input and output files.
SeparateInputOutput(Vec<BuildInfoBufferSeparateOutput<'a>>),
}

impl BuildInfoBuffers<'_> {
fn parse(&self) -> Result<Vec<BuildInfoWithOutput>, BuildInfoConfigError> {
fn filter_on_solc_version(
build_info: BuildInfoWithOutput,
) -> Result<Option<BuildInfoWithOutput>, BuildInfoConfigError> {
let solc_version = build_info.solc_version.parse::<semver::Version>()?;

if crate::compiler::FIRST_SOLC_VERSION_SUPPORTED <= solc_version {
Ok(Some(build_info))
} else {
Ok(None)
}
}

match self {
BuildInfoBuffers::WithOutput(build_infos_with_output) => build_infos_with_output
.iter()
.map(|item| {
let build_info: BuildInfoWithOutput = serde_json::from_slice(item)?;
filter_on_solc_version(build_info)
})
.flatten_ok()
.collect::<Result<Vec<BuildInfoWithOutput>, _>>(),
BuildInfoBuffers::SeparateInputOutput(separate_output) => separate_output
.iter()
.map(|item| {
let input: BuildInfo = serde_json::from_slice(item.build_info)?;
let output: BuildInfoOutput = serde_json::from_slice(item.output)?;
// Make sure we get the output matching the input.
if input.id != output.id {
return Err(BuildInfoConfigError::InputOutputMismatch {
input_id: input.id,
output_id: output.id,
});
}
filter_on_solc_version(BuildInfoWithOutput {
_format: input._format,
id: input.id,
solc_version: input.solc_version,
solc_long_version: input.solc_long_version,
input: input.input,
output: output.output,
})
})
.flatten_ok()
.collect::<Result<Vec<BuildInfoWithOutput>, _>>(),
}
}
}

/// Separate build info input and output files.
#[derive(Clone, Debug)]
pub struct BuildInfoBufferSeparateOutput<'a> {
/// Deserializes to `BuildInfo`
pub build_info: &'a [u8],
/// Deserializes to `BuildInfoOutput`
pub output: &'a [u8],
}

/// A `BuildInfoWithOutput` contains all the information of a solc run. It
/// includes all the necessary information to recreate that exact same run, and
/// all of its output.
/// the output of the run.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfoWithOutput {
#[serde(rename = "_format")]
pub _format: String,
pub id: String,
pub solc_version: String,
pub solc_long_version: String,
pub input: CompilerInput,
pub output: CompilerOutput,
}

/// A `BuildInfo` contains all the input information of a solc run. It
/// includes all the necessary information to recreate that exact same run.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfo {
Expand All @@ -21,6 +158,15 @@ pub struct BuildInfo {
pub solc_version: String,
pub solc_long_version: String,
pub input: CompilerInput,
}

/// A `BuildInfoOutput` contains all the output of a solc run.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfoOutput {
#[serde(rename = "_format")]
pub _format: String,
pub id: String,
pub output: CompilerOutput,
}

Expand Down
3 changes: 3 additions & 0 deletions crates/edr_solidity/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use crate::{
source_map::decode_instructions,
};

/// First Solc version supported for stack trace generation
pub const FIRST_SOLC_VERSION_SUPPORTED: semver::Version = semver::Version::new(0, 5, 1);

/// For the Solidity compiler version and its standard JSON input and
/// output[^1], creates the source model, decodes the bytecode with source
/// mapping and links them to the source files.
Expand Down
Loading

0 comments on commit af26624

Please sign in to comment.