Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: pass build info as buffer #780

Merged
merged 10 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading