From 7d1e91af23b220f33e33e1aa140985d4ec86c1be Mon Sep 17 00:00:00 2001 From: Wodann Date: Tue, 7 Mar 2023 13:44:23 -0500 Subject: [PATCH 1/6] improvement: minimize data copied to JS --- crates/rethnet_eth/Cargo.toml | 2 +- crates/rethnet_evm/Cargo.toml | 2 +- crates/rethnet_evm_napi/Cargo.toml | 2 +- crates/rethnet_evm_napi/src/runtime.rs | 35 ++- crates/rethnet_evm_napi/src/state.rs | 34 ++- .../rethnet_evm_napi/src/tracer/js_tracer.rs | 259 +++++++++++------- .../hardhat-network/provider/vm/dual.ts | 118 ++++---- .../hardhat-network/provider/vm/ethereumjs.ts | 30 +- .../hardhat-network/provider/vm/rethnet.ts | 4 +- 9 files changed, 289 insertions(+), 197 deletions(-) diff --git a/crates/rethnet_eth/Cargo.toml b/crates/rethnet_eth/Cargo.toml index 45594fb6b8..559440fe93 100644 --- a/crates/rethnet_eth/Cargo.toml +++ b/crates/rethnet_eth/Cargo.toml @@ -14,7 +14,7 @@ hex-literal = { version = "0.3", default-features = false } open-fastrlp = { version = "0.1.2", default-features = false, features = ["derive"], optional = true } primitive-types = { version = "0.11.1", default-features = false, features = ["rlp"] } reqwest = { version = "0.11", features = ["blocking", "json"] } -revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "8e6f4f2", version = "1.0", default-features = false } +revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = "1.0", default-features = false } # revm-primitives = { path = "../../../revm/crates/primitives", version = "1.0", default-features = false } rlp = { version = "0.5.2", default-features = false, features = ["derive"] } ruint = { version = "1.7.0", default-features = false } diff --git a/crates/rethnet_evm/Cargo.toml b/crates/rethnet_evm/Cargo.toml index 5aa495b2d3..b85cfa0e66 100644 --- a/crates/rethnet_evm/Cargo.toml +++ b/crates/rethnet_evm/Cargo.toml @@ -9,7 +9,7 @@ hashbrown = { version = "0.13", default-features = false, features = ["ahash", " log = { version = "0.4.17", default-features = false } parking_lot = { version = "0.12.1", default-features = false } rethnet_eth = { version = "0.1.0-dev", path = "../rethnet_eth", features = ["serde"] } -revm = { git = "https://github.com/bluealloy/revm", rev = "8e6f4f2", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } +revm = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } # revm = { path = "../../../revm/crates/revm", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } thiserror = { version = "1.0.38", default-features = false } tokio = { version = "1.21.2", default-features = false, features = ["rt-multi-thread", "sync"] } diff --git a/crates/rethnet_evm_napi/Cargo.toml b/crates/rethnet_evm_napi/Cargo.toml index 1d4cfc2660..ef01898c36 100644 --- a/crates/rethnet_evm_napi/Cargo.toml +++ b/crates/rethnet_evm_napi/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib"] [dependencies] crossbeam-channel = { version = "0.5.6", default-features = false } -# if ever napi needs to be pinned, be sure to pin napi-derive to the same version +# when napi is pinned, be sure to pin napi-derive to the same version napi = { version = "2.12.4", default-features = false, features = ["async", "error_anyhow", "napi8", "serde-json"] } napi-derive = "2.12.3" once_cell = "1.15.0" diff --git a/crates/rethnet_evm_napi/src/runtime.rs b/crates/rethnet_evm_napi/src/runtime.rs index afffabf149..ebd0f896a4 100644 --- a/crates/rethnet_evm_napi/src/runtime.rs +++ b/crates/rethnet_evm_napi/src/runtime.rs @@ -11,10 +11,7 @@ use crate::{ config::Config, state::StateManager, tracer::Tracer, - transaction::{ - result::{ExecutionResult, TransactionResult}, - Transaction, - }, + transaction::{result::ExecutionResult, Transaction}, }; struct Logger; @@ -61,18 +58,19 @@ impl Rethnet { transaction: Transaction, block: BlockConfig, tracer: Option<&Tracer>, - ) -> napi::Result { + ) -> napi::Result { let transaction = TxEnv::try_from(transaction)?; let block = BlockEnv::try_from(block)?; let inspector = tracer.map(|tracer| tracer.as_dyn_inspector()); - TransactionResult::try_from( - self.runtime - .dry_run(transaction, block, inspector) - .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?, - ) + let (result, _state, trace) = self + .runtime + .dry_run(transaction, block, inspector) + .await + .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; + + Ok(ExecutionResult::from((result, trace))) } /// Executes the provided transaction without changing state, ignoring validation checks in the process. @@ -82,18 +80,19 @@ impl Rethnet { transaction: Transaction, block: BlockConfig, tracer: Option<&Tracer>, - ) -> napi::Result { + ) -> napi::Result { let transaction = TxEnv::try_from(transaction)?; let block = BlockEnv::try_from(block)?; let inspector = tracer.map(|tracer| tracer.as_dyn_inspector()); - TransactionResult::try_from( - self.runtime - .guaranteed_dry_run(transaction, block, inspector) - .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?, - ) + let (result, _state, trace) = self + .runtime + .guaranteed_dry_run(transaction, block, inspector) + .await + .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; + + Ok(ExecutionResult::from((result, trace))) } /// Executes the provided transaction, changing state in the process. diff --git a/crates/rethnet_evm_napi/src/state.rs b/crates/rethnet_evm_napi/src/state.rs index 1a909019fb..9b2c10278d 100644 --- a/crates/rethnet_evm_napi/src/state.rs +++ b/crates/rethnet_evm_napi/src/state.rs @@ -1,11 +1,14 @@ -use std::sync::{ - mpsc::{channel, Sender}, - Arc, +use std::{ + mem, + sync::{ + mpsc::{channel, Sender}, + Arc, + }, }; use napi::{bindgen_prelude::*, JsFunction, JsObject, NapiRaw, Status}; use napi_derive::napi; -use rethnet_eth::{signature::private_key_to_address, Address, B256, U256}; +use rethnet_eth::{signature::private_key_to_address, Address, Bytes, B256, U256}; use rethnet_evm::{ state::{AsyncState, LayeredState, RethnetLayer, StateDebug, StateError, SyncState}, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY, @@ -238,7 +241,7 @@ impl StateManager { env.raw(), unsafe { modify_account_fn.raw() }, 0, - |ctx: ThreadSafeCallContext| { + |mut ctx: ThreadSafeCallContext| { let sender = ctx.value.sender.clone(); let balance = ctx @@ -258,9 +261,26 @@ impl StateManager { .create_buffer_copy(code.hash()) .and_then(|hash| bytecode.set_named_property("hash", hash.into_raw()))?; + let code = code.original_bytes(); + ctx.env - .create_buffer_copy(&code.bytes()[..code.len()]) - .and_then(|code| bytecode.set_named_property("code", code.into_raw()))?; + .adjust_external_memory(code.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + code.as_ptr(), + code.len(), + code, + |code: Bytes, mut env| { + env.adjust_external_memory(-(code.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(code); + }, + ) + } + .and_then(|code| bytecode.set_named_property("code", code.into_raw()))?; bytecode.into_unknown() } else { diff --git a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs index c2f704ea45..005e3ea183 100644 --- a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs +++ b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs @@ -1,5 +1,6 @@ use std::{ fmt::Debug, + mem, sync::mpsc::{channel, Sender}, }; @@ -9,12 +10,9 @@ use napi::{ }; use napi_derive::napi; use rethnet_eth::{Address, Bytes, U256}; -use rethnet_evm::{ - opcode, return_revert, Bytecode, Gas, InstructionResult, SuccessOrHalt, OPCODE_JUMPMAP, -}; +use rethnet_evm::{opcode, return_revert, Bytecode, Gas, InstructionResult, SuccessOrHalt}; use crate::{ - account::Account, sync::{await_void_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, transaction::result::{ExceptionalHalt, ExecutionResult}, @@ -56,33 +54,33 @@ pub struct TracingStep { /// The program counter #[napi(readonly)] pub pc: BigInt, - /// The executed op code - #[napi(readonly)] - pub opcode: String, + // /// The executed op code + // #[napi(readonly)] + // pub opcode: String, // /// The return value of the step // #[napi(readonly)] // pub return_value: u8, - /// The amount of gas that was used by the step - #[napi(readonly)] - pub gas_cost: BigInt, - /// The amount of gas that was refunded by the step - #[napi(readonly)] - pub gas_refunded: BigInt, - /// The amount of gas left - #[napi(readonly)] - pub gas_left: BigInt, - /// The stack - #[napi(readonly)] - pub stack: Vec, - /// The memory - #[napi(readonly)] - pub memory: Buffer, - /// The contract being executed - #[napi(readonly)] - pub contract: Account, - /// The address of the contract - #[napi(readonly)] - pub contract_address: Buffer, + // /// The amount of gas that was used by the step + // #[napi(readonly)] + // pub gas_cost: BigInt, + // /// The amount of gas that was refunded by the step + // #[napi(readonly)] + // pub gas_refunded: BigInt, + // /// The amount of gas left + // #[napi(readonly)] + // pub gas_left: BigInt, + // /// The stack + // #[napi(readonly)] + // pub stack: Vec, + // /// The memory + // #[napi(readonly)] + // pub memory: Buffer, + // /// The contract being executed + // #[napi(readonly)] + // pub contract: Account, + // /// The address of the contract + // #[napi(readonly)] + // pub contract_address: Buffer, // /// The address of the code being executed // #[napi(readonly)] // pub code_address: Buffer, @@ -168,7 +166,7 @@ impl JsTracer { env.raw(), unsafe { callbacks.before_message.raw() }, 0, - |ctx: ThreadSafeCallContext| { + |mut ctx: ThreadSafeCallContext| { let sender = ctx.value.sender.clone(); let mut tracing_message = ctx.env.create_object()?; @@ -191,9 +189,26 @@ impl JsTracer { ) .and_then(|to| tracing_message.set_named_property("to", to))?; + let data = ctx.value.message.data; + ctx.env - .create_buffer_copy(&ctx.value.message.data) - .and_then(|data| tracing_message.set_named_property("data", data.into_raw()))?; + .adjust_external_memory(data.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + data.as_ptr(), + data.len(), + data, + |data: Bytes, mut env| { + env.adjust_external_memory(-(data.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(data); + }, + ) + } + .and_then(|data| tracing_message.set_named_property("data", data.into_raw()))?; ctx.env .create_bigint_from_words(false, ctx.value.message.value.as_limbs().to_vec()) @@ -215,21 +230,32 @@ impl JsTracer { tracing_message.set_named_property("codeAddress", code_address) })?; - ctx.value - .message - .code - .as_ref() - .map_or_else( - || ctx.env.get_undefined().map(JsUndefined::into_unknown), - |code| { - ctx.env - .create_buffer_copy(&code.bytes()[..code.len()]) - .map(JsBufferValue::into_unknown) - }, - ) - .and_then(|code_address| { - tracing_message.set_named_property("code", code_address) - })?; + if let Some(code) = &ctx.value.message.code { + let code = code.original_bytes(); + ctx.env + .adjust_external_memory(code.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + code.as_ptr(), + code.len(), + code, + |code: Bytes, mut env| { + env.adjust_external_memory(-(code.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(code); + }, + ) + } + .map(JsBufferValue::into_unknown) + } else { + ctx.env.get_undefined().map(JsUndefined::into_unknown) + } + .and_then(|code_address| { + tracing_message.set_named_property("code", code_address) + })?; let next = ctx.env.create_object()?; @@ -257,9 +283,9 @@ impl JsTracer { .create_bigint_from_u64(ctx.value.pc) .and_then(|pc| tracing_step.set_named_property("pc", pc))?; - ctx.env - .create_string(OPCODE_JUMPMAP[usize::from(ctx.value.opcode)].unwrap_or("")) - .and_then(|opcode| tracing_step.set_named_property("opcode", opcode))?; + // ctx.env + // .create_string(OPCODE_JUMPMAP[usize::from(ctx.value.opcode)].unwrap_or("")) + // .and_then(|opcode| tracing_step.set_named_property("opcode", opcode))?; // ctx.env // .create_uint32((ctx.value.return_value as u8).into()) @@ -303,44 +329,44 @@ impl JsTracer { // tracing_step.set_named_property("memory", memory.into_raw()) // })?; - let mut contract = ctx.env.create_object()?; + // let mut contract = ctx.env.create_object()?; - ctx.env - .create_bigint_from_words(false, ctx.value.contract.balance.as_limbs().to_vec()) - .and_then(|balance| contract.set_named_property("balance", balance))?; + // ctx.env + // .create_bigint_from_words(false, ctx.value.contract.balance.as_limbs().to_vec()) + // .and_then(|balance| contract.set_named_property("balance", balance))?; - let nonce = ctx.env.create_bigint_from_u64(ctx.value.contract.nonce)?; - contract.set_named_property("nonce", nonce)?; + // let nonce = ctx.env.create_bigint_from_u64(ctx.value.contract.nonce)?; + // contract.set_named_property("nonce", nonce)?; - ctx.env - .create_buffer_copy(ctx.value.contract.code_hash) - .and_then(|code_hash| { - contract.set_named_property("codeHash", code_hash.into_unknown()) - })?; - - ctx.value - .contract - .code - .as_ref() - .map_or_else( - || ctx.env.get_undefined().map(JsUndefined::into_unknown), - |code| { - ctx.env - .create_buffer_copy(&code.bytes()[..code.len()]) - .map(|code| code.into_unknown()) - }, - ) - .and_then(|code| contract.set_named_property("code", code))?; - - tracing_step.set_named_property("contract", contract)?; + // ctx.env + // .create_buffer_copy(ctx.value.contract.code_hash) + // .and_then(|code_hash| { + // contract.set_named_property("codeHash", code_hash.into_unknown()) + // })?; - let contract_address = &ctx.value.contract_address; - ctx.env - .create_buffer_copy(contract_address) - .and_then(|contract_address| { - tracing_step - .set_named_property("contractAddress", contract_address.into_unknown()) - })?; + // ctx.value + // .contract + // .code + // .as_ref() + // .map_or_else( + // || ctx.env.get_undefined().map(JsUndefined::into_unknown), + // |code| { + // ctx.env + // .create_buffer_copy(&code.bytes()[..code.len()]) + // .map(|code| code.into_unknown()) + // }, + // ) + // .and_then(|code| contract.set_named_property("code", code))?; + + // tracing_step.set_named_property("contract", contract)?; + + // let contract_address = &ctx.value.contract_address; + // ctx.env + // .create_buffer_copy(contract_address) + // .and_then(|contract_address| { + // tracing_step + // .set_named_property("contractAddress", contract_address.into_unknown()) + // })?; let next = ctx.env.create_object()?; @@ -355,7 +381,7 @@ impl JsTracer { env.raw(), unsafe { callbacks.after_message.raw() }, 0, - |ctx: ThreadSafeCallContext| { + |mut ctx: ThreadSafeCallContext| { let sender = ctx.value.sender.clone(); let mut tracing_message_result = ctx.env.create_object()?; @@ -412,7 +438,24 @@ impl JsTracer { log_object.set_named_property("topics", topics) })?; - ctx.env.create_buffer_copy(&log.data).and_then(|data| { + ctx.env + .adjust_external_memory(log.data.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + log.data.as_ptr(), + log.data.len(), + log.data, + |data: Bytes, mut env| { + env.adjust_external_memory(-(data.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(data); + }, + ) + } + .and_then(|data| { log_object.set_named_property("data", data.into_raw()) })?; @@ -431,11 +474,26 @@ impl JsTracer { let mut transaction_output = ctx.env.create_object()?; ctx.env - .create_buffer_copy(output) - .map(JsBufferValue::into_unknown) - .and_then(|output| { - transaction_output.set_named_property("returnValue", output) - })?; + .adjust_external_memory(output.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + output.as_ptr(), + output.len(), + output, + |output: Bytes, mut env| { + env.adjust_external_memory(-(output.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(output); + }, + ) + } + .map(JsBufferValue::into_unknown) + .and_then(|output| { + transaction_output.set_named_property("returnValue", output) + })?; address .map_or_else( @@ -456,9 +514,24 @@ impl JsTracer { } rethnet_evm::ExecutionResult::Revert { gas_used, output } => { ctx.env - .create_buffer_copy(output) - .map(JsBufferValue::into_unknown) - .and_then(|output| result.set_named_property("output", output))?; + .adjust_external_memory(output.len() as i64) + .expect("Failed to adjust external memory"); + + unsafe { + ctx.env.create_buffer_with_borrowed_data( + output.as_ptr(), + output.len(), + output, + |output: Bytes, mut env| { + env.adjust_external_memory(-(output.len() as i64)) + .expect("Failed to adjust external memory"); + + mem::forget(output); + }, + ) + } + .map(JsBufferValue::into_unknown) + .and_then(|output| result.set_named_property("output", output))?; gas_used } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts index c9839d28c1..699ffc809c 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts @@ -424,67 +424,67 @@ export class DualModeAdapter implements VMAdapter { throw new Error("Different step pc"); } - if (ethereumJSStep.opcode !== rethnetStep.opcode) { - console.trace( - `Different steps[${stepIdx}] opcode: ${ethereumJSStep.opcode} !== ${rethnetStep.opcode}` - ); - throw new Error("Different step opcode"); - } + // if (ethereumJSStep.opcode !== rethnetStep.opcode) { + // console.trace( + // `Different steps[${stepIdx}] opcode: ${ethereumJSStep.opcode} !== ${rethnetStep.opcode}` + // ); + // throw new Error("Different step opcode"); + // } - if (ethereumJSStep.gasCost !== rethnetStep.gasCost) { - console.trace( - `Different steps[${stepIdx}] gasCost: ${ethereumJSStep.gasCost} !== ${rethnetStep.gasCost}` - ); - throw new Error("Different step gasCost"); - } + // if (ethereumJSStep.gasCost !== rethnetStep.gasCost) { + // console.trace( + // `Different steps[${stepIdx}] gasCost: ${ethereumJSStep.gasCost} !== ${rethnetStep.gasCost}` + // ); + // throw new Error("Different step gasCost"); + // } - if (ethereumJSStep.gasLeft !== rethnetStep.gasLeft) { - console.trace( - `Different steps[${stepIdx}] gasLeft: ${ethereumJSStep.gasLeft} !== ${rethnetStep.gasLeft}` - ); - throw new Error("Different step gasLeft"); - } + // if (ethereumJSStep.gasLeft !== rethnetStep.gasLeft) { + // console.trace( + // `Different steps[${stepIdx}] gasLeft: ${ethereumJSStep.gasLeft} !== ${rethnetStep.gasLeft}` + // ); + // throw new Error("Different step gasLeft"); + // } - const ethereumJSStack = ethereumJSStep.stack; - const rethnetStack = rethnetStep.stack; - if (ethereumJSStack.length !== rethnetStack.length) { - throw new Error( - `Different number of stack elements in tracers: ${ethereumJSStack.length} !== ${rethnetStack.length}` - ); - } + // const ethereumJSStack = ethereumJSStep.stack; + // const rethnetStack = rethnetStep.stack; + // if (ethereumJSStack.length !== rethnetStack.length) { + // throw new Error( + // `Different number of stack elements in tracers: ${ethereumJSStack.length} !== ${rethnetStack.length}` + // ); + // } - for (let stackIdx = 0; stackIdx < ethereumJSSteps.length; ++stackIdx) { - const ethereumJSStackElement = ethereumJSStack[stackIdx]; - const rethnetStackElement = rethnetStack[stackIdx]; + // for (let stackIdx = 0; stackIdx < ethereumJSSteps.length; ++stackIdx) { + // const ethereumJSStackElement = ethereumJSStack[stackIdx]; + // const rethnetStackElement = rethnetStack[stackIdx]; - if (ethereumJSStackElement !== rethnetStackElement) { - console.trace( - `Different steps[${stepIdx}] stack[${stackIdx}]: ${ethereumJSStackElement} !== ${rethnetStackElement}` - ); - throw new Error("Different step stack element"); - } - } + // if (ethereumJSStackElement !== rethnetStackElement) { + // console.trace( + // `Different steps[${stepIdx}] stack[${stackIdx}]: ${ethereumJSStackElement} !== ${rethnetStackElement}` + // ); + // throw new Error("Different step stack element"); + // } + // } - if (!ethereumJSStep.memory.equals(rethnetStep.memory)) { - console.trace( - `Different steps[${stepIdx}] memory: ${ethereumJSStep.memory} !== ${rethnetStep.memory}` - ); - throw new Error("Different step memory"); - } + // if (!ethereumJSStep.memory.equals(rethnetStep.memory)) { + // console.trace( + // `Different steps[${stepIdx}] memory: ${ethereumJSStep.memory} !== ${rethnetStep.memory}` + // ); + // throw new Error("Different step memory"); + // } - if (ethereumJSStep.contract.balance !== rethnetStep.contract.balance) { - console.trace( - `Different steps[${stepIdx}] contract balance: ${ethereumJSStep.contract.balance} !== ${rethnetStep.contract.balance}` - ); - throw new Error("Different step contract balance"); - } + // if (ethereumJSStep.contract.balance !== rethnetStep.contract.balance) { + // console.trace( + // `Different steps[${stepIdx}] contract balance: ${ethereumJSStep.contract.balance} !== ${rethnetStep.contract.balance}` + // ); + // throw new Error("Different step contract balance"); + // } - if (ethereumJSStep.contract.nonce !== rethnetStep.contract.nonce) { - console.trace( - `Different steps[${stepIdx}] contract nonce: ${ethereumJSStep.contract.nonce} !== ${rethnetStep.contract.nonce}` - ); - throw new Error("Different step contract nonce"); - } + // if (ethereumJSStep.contract.nonce !== rethnetStep.contract.nonce) { + // console.trace( + // `Different steps[${stepIdx}] contract nonce: ${ethereumJSStep.contract.nonce} !== ${rethnetStep.contract.nonce}` + // ); + // throw new Error("Different step contract nonce"); + // } // Code can be stored separately from the account in Rethnet // const ethereumJSCode = ethereumJSStep.contract.code; @@ -518,12 +518,12 @@ export class DualModeAdapter implements VMAdapter { // } // } - if (!ethereumJSStep.contractAddress.equals(rethnetStep.contractAddress)) { - console.trace( - `Different steps[${stepIdx}] contract address: ${ethereumJSStep.contractAddress} !== ${rethnetStep.contractAddress}` - ); - throw new Error("Different step contract address"); - } + // if (!ethereumJSStep.contractAddress.equals(rethnetStep.contractAddress)) { + // console.trace( + // `Different steps[${stepIdx}] contract address: ${ethereumJSStep.contractAddress} !== ${rethnetStep.contractAddress}` + // ); + // throw new Error("Different step contract address"); + // } } // TODO: compare each step diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts index 8c10fe784b..a6129bb513 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts @@ -519,22 +519,22 @@ export class EthereumJSAdapter implements VMAdapter { await this._vmTracer.addStep({ depth: step.depth, pc: BigInt(step.pc), - opcode: step.opcode.name, + // opcode: step.opcode.name, // returnValue: 0, // Do we have error values in ethereumjs? - gasCost: BigInt(step.opcode.fee) + (step.opcode.dynamicFee ?? 0n), - gasRefunded: step.gasRefund, - gasLeft: step.gasLeft, - stack: step.stack, - memory: step.memory, - contract: { - balance: step.account.balance, - nonce: step.account.nonce, - code: { - hash: step.account.codeHash, - code: Buffer.from([]), - }, - }, - contractAddress: step.address.buf, + // gasCost: BigInt(step.opcode.fee) + (step.opcode.dynamicFee ?? 0n), + // gasRefunded: step.gasRefund, + // gasLeft: step.gasLeft, + // stack: step.stack, + // memory: step.memory, + // contract: { + // balance: step.account.balance, + // nonce: step.account.nonce, + // code: { + // hash: step.account.codeHash, + // code: Buffer.from([]), + // }, + // }, + // contractAddress: step.address.buf, }); return next(); diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts index 172bfae15e..bcfe420f62 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts @@ -130,10 +130,10 @@ export class RethnetAdapter implements VMAdapter { try { const result = rethnetResultToRunTxResult( - rethnetResult.execResult, + rethnetResult, blockContext.header.gasUsed ); - return [result, rethnetResult.execResult.trace]; + return [result, rethnetResult.trace]; } catch (e) { // console.log("Rethnet trace"); // console.log(rethnetResult.execResult.trace); From caffe70fdaa39e043028e8e0bd2b0beb455e0182 Mon Sep 17 00:00:00 2001 From: Wodann Date: Wed, 29 Mar 2023 12:44:34 -0500 Subject: [PATCH 2/6] fix: remove self references in Hardhat objects --- .../hardhat-network/provider/TxPool.ts | 49 +- .../provider/modules/hardhat.ts | 5 +- .../internal/hardhat-network/provider/node.ts | 26 +- .../hardhat-network/provider/provider.ts | 19 +- .../hardhat-network/provider/vm/ethereumjs.ts | 30 +- .../hardhat-network/provider/vm/rethnet.ts | 28 +- .../hardhat-network/provider/TxPool.ts | 538 ++++++++++++++---- 7 files changed, 517 insertions(+), 178 deletions(-) diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/TxPool.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/TxPool.ts index d11d956a9e..60584ca8f8 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/TxPool.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/TxPool.ts @@ -100,23 +100,33 @@ export class TxPool { tx: SerializedTransaction ) => OrderedTransaction; - constructor( - private readonly _getAccount: (address: Address) => Promise, - blockGasLimit: bigint, - common: Common - ) { + constructor(blockGasLimit: bigint, common: Common) { this._state = makePoolState({ blockGasLimit: BigIntUtils.toHex(blockGasLimit), }); this._deserializeTransaction = (tx) => deserializeTransaction(tx, common); } - public async addTransaction(tx: TypedTransaction) { + public async addTransaction( + getAccount: (address: Address) => Promise, + tx: TypedTransaction + ) { const senderAddress = this._getSenderAddress(tx); - const nextConfirmedNonce = await this._getNextConfirmedNonce(senderAddress); - const nextPendingNonce = await this.getNextPendingNonce(senderAddress); + const nextConfirmedNonce = await this._getNextConfirmedNonce( + getAccount, + senderAddress + ); + const nextPendingNonce = await this.getNextPendingNonce( + getAccount, + senderAddress + ); - await this._validateTransaction(tx, senderAddress, nextConfirmedNonce); + await this._validateTransaction( + getAccount, + tx, + senderAddress, + nextConfirmedNonce + ); const txNonce = tx.nonce; @@ -245,12 +255,15 @@ export class TxPool { * Returns the next available nonce for an address, taking into account * its pending transactions. */ - public async getNextPendingNonce(accountAddress: Address): Promise { + public async getNextPendingNonce( + getAccount: (address: Address) => Promise, + accountAddress: Address + ): Promise { const pendingTxs = this._getPendingForAddress(accountAddress.toString()); const lastPendingTx = pendingTxs?.last(undefined); if (lastPendingTx === undefined) { - return this._getNextConfirmedNonce(accountAddress); + return this._getNextConfirmedNonce(getAccount, accountAddress); } const lastPendingTxNonce = @@ -273,12 +286,14 @@ export class TxPool { /** * Updates the pending and queued list of all addresses */ - public async updatePendingAndQueued() { + public async updatePendingAndQueued( + getAccount: (address: Address) => Promise + ) { let newPending = this._getPending(); // update pending transactions for (const [address, txs] of newPending) { - const senderAccount = await this._getAccount(Address.fromString(address)); + const senderAccount = await getAccount(Address.fromString(address)); const senderNonce = senderAccount.nonce; const senderBalance = senderAccount.balance; @@ -314,7 +329,7 @@ export class TxPool { // update queued addresses let newQueued = this._getQueued(); for (const [address, txs] of newQueued) { - const senderAccount = await this._getAccount(Address.fromString(address)); + const senderAccount = await getAccount(Address.fromString(address)); const senderNonce = senderAccount.nonce; const senderBalance = senderAccount.balance; @@ -421,6 +436,7 @@ export class TxPool { } private async _validateTransaction( + getAccount: (address: Address) => Promise, tx: TypedTransaction, senderAddress: Address, senderNonce: bigint @@ -440,7 +456,7 @@ export class TxPool { ); } - const senderAccount = await this._getAccount(senderAddress); + const senderAccount = await getAccount(senderAddress); const senderBalance = senderAccount.balance; const maxFee = "gasPrice" in tx ? tx.gasPrice : tx.maxFeePerGas; @@ -584,9 +600,10 @@ export class TxPool { * pending transactions. */ private async _getNextConfirmedNonce( + getAccount: (address: Address) => Promise, accountAddress: Address ): Promise { - const account = await this._getAccount(accountAddress); + const account = await getAccount(accountAddress); return account.nonce; } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/modules/hardhat.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/modules/hardhat.ts index 428a92b3c5..14398880ca 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/modules/hardhat.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/modules/hardhat.ts @@ -40,9 +40,6 @@ export class HardhatModule { constructor( private readonly _node: HardhatNode, private readonly _resetCallback: (forkConfig?: ForkConfig) => Promise, - private readonly _setLoggingEnabledCallback: ( - loggingEnabled: boolean - ) => void, private readonly _logger: ModulesLogger, private readonly _experimentalHardhatNetworkMessageTraceHooks: BoundExperimentalHardhatNetworkMessageTraceHook[] = [] ) {} @@ -241,7 +238,7 @@ export class HardhatModule { private async _setLoggingEnabledAction( loggingEnabled: boolean ): Promise { - this._setLoggingEnabledCallback(loggingEnabled); + this._logger.setEnabled(loggingEnabled); return true; } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts index 9d459b677c..36f72d7d62 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts @@ -239,11 +239,7 @@ export class HardhatNode extends EventEmitter { ); } - const txPool = new TxPool( - (address) => vm.getAccount(address), - BigInt(blockGasLimit), - common - ); + const txPool = new TxPool(BigInt(blockGasLimit), common); const instanceId = bufferToBigInt(randomBytes(32)); @@ -647,7 +643,10 @@ Hardhat Network's forking functionality only works with blocks from at least spu } public async getAccountNextPendingNonce(address: Address): Promise { - return this._txPool.getNextPendingNonce(address); + return this._txPool.getNextPendingNonce( + this._vm.getAccount.bind(this._vm), + address + ); } public async getCodeFromTrace( @@ -1225,7 +1224,9 @@ Hardhat Network's forking functionality only works with blocks from at least spu public async setBlockGasLimit(gasLimit: bigint | number) { this._txPool.setBlockGasLimit(gasLimit); - await this._txPool.updatePendingAndQueued(); + await this._txPool.updatePendingAndQueued( + this._vm.getAccount.bind(this._vm) + ); } public async setMinGasPrice(minGasPrice: bigint) { @@ -1505,7 +1506,7 @@ Hardhat Network's forking functionality only works with blocks from at least spu } private async _addPendingTransaction(tx: TypedTransaction): Promise { - await this._txPool.addTransaction(tx); + await this._txPool.addTransaction(this._vm.getAccount.bind(this._vm), tx); await this._notifyPendingTransaction(tx); return bufferToHex(tx.hash()); } @@ -1601,7 +1602,10 @@ Hardhat Network's forking functionality only works with blocks from at least spu } // validate nonce - const nextPendingNonce = await this._txPool.getNextPendingNonce(sender); + const nextPendingNonce = await this._txPool.getNextPendingNonce( + this._vm.getAccount.bind(this._vm), + sender + ); const txNonce = tx.nonce; const expectedNonceMsg = `Expected nonce to be ${nextPendingNonce.toString()} but got ${txNonce.toString()}.`; @@ -1717,7 +1721,9 @@ Hardhat Network's forking functionality only works with blocks from at least spu const block = await blockBuilder.seal(); await this._blockchain.putBlock(block); - await this._txPool.updatePendingAndQueued(); + await this._txPool.updatePendingAndQueued( + this._vm.getAccount.bind(this._vm) + ); return { block, diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts index 33abfcacbb..14c6dc8fda 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts @@ -272,12 +272,12 @@ export class HardhatNetworkProvider this._logger, this._experimentalHardhatNetworkMessageTraceHooks ); + + const provider = new WeakRef(this); this._hardhatModule = new HardhatModule( node, - (forkConfig?: ForkConfig) => this._reset(miningTimer, forkConfig), - (loggingEnabled: boolean) => { - this._logger.setEnabled(loggingEnabled); - }, + (forkConfig?: ForkConfig) => + provider.deref()!._reset(miningTimer, forkConfig), this._logger, this._experimentalHardhatNetworkMessageTraceHooks ); @@ -320,9 +320,10 @@ export class HardhatNetworkProvider } private _makeMiningTimer(): MiningTimer { + const provider = new WeakRef(this); const miningTimer = new MiningTimer(this._intervalMining, async () => { try { - await this.request({ method: "hardhat_intervalMine" }); + await provider.deref()!.request({ method: "hardhat_intervalMine" }); } catch (e) { console.error("Unexpected error calling hardhat_intervalMine:", e); } @@ -346,19 +347,19 @@ export class HardhatNetworkProvider } private _forwardNodeEvents(node: HardhatNode) { - node.addListener("ethEvent", this._ethEventListener); + node.addListener("ethEvent", this._ethEventListener.bind(this)); } private _stopForwardingNodeEvents(node: HardhatNode) { - node.removeListener("ethEvent", this._ethEventListener); + node.removeListener("ethEvent", this._ethEventListener.bind(this)); } - private _ethEventListener = (payload: { filterId: bigint; result: any }) => { + private _ethEventListener(payload: { filterId: bigint; result: any }) { const subscription = `0x${payload.filterId.toString(16)}`; const result = payload.result; this._emitLegacySubscriptionEvent(subscription, result); this._emitEip1193SubscriptionEvent(subscription, result); - }; + } private _emitLegacySubscriptionEvent(subscription: string, result: any) { this.emit("notification", { diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts index a6129bb513..73d28562ed 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts @@ -67,9 +67,15 @@ export class EthereumJSAdapter implements VMAdapter { "EVM should have an 'events' property" ); - this._vm.evm.events.on("beforeMessage", this._beforeMessageHandler); - this._vm.evm.events.on("step", this._stepHandler); - this._vm.evm.events.on("afterMessage", this._afterMessageHandler); + this._vm.evm.events.on( + "beforeMessage", + this._beforeMessageHandler.bind(this) + ); + this._vm.evm.events.on("step", this._stepHandler.bind(this)); + this._vm.evm.events.on( + "afterMessage", + this._afterMessageHandler.bind(this) + ); } public static async create( @@ -494,7 +500,10 @@ export class EthereumJSAdapter implements VMAdapter { return this._common.gteHardfork("london"); } - private _beforeMessageHandler = async (message: Message, next: any) => { + private async _beforeMessageHandler( + message: Message, + next: any + ): Promise { try { const code = message.to !== undefined @@ -512,9 +521,9 @@ export class EthereumJSAdapter implements VMAdapter { } catch (e) { return next(e); } - }; + } - private _stepHandler = async (step: InterpreterStep, next: any) => { + private async _stepHandler(step: InterpreterStep, next: any): Promise { try { await this._vmTracer.addStep({ depth: step.depth, @@ -541,9 +550,12 @@ export class EthereumJSAdapter implements VMAdapter { } catch (e) { return next(e); } - }; + } - private _afterMessageHandler = async (result: EVMResult, next: any) => { + private async _afterMessageHandler( + result: EVMResult, + next: any + ): Promise { try { const gasUsed = result.execResult.executionGasUsed; @@ -611,5 +623,5 @@ export class EthereumJSAdapter implements VMAdapter { } catch (e) { return next(e); } - }; + } } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts index bcfe420f62..b5ef329071 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts @@ -108,9 +108,9 @@ export class RethnetAdapter implements VMAdapter { ); const tracer = new Tracer({ - beforeMessage: this._beforeMessageHandler, - step: this._stepHandler, - afterMessage: this._afterMessageHandler, + beforeMessage: this._beforeMessageHandler.bind(this), + step: this._stepHandler.bind(this), + afterMessage: this._afterMessageHandler.bind(this), }); const rethnetResult = await this._rethnet.guaranteedDryRun( @@ -302,9 +302,9 @@ export class RethnetAdapter implements VMAdapter { ); const tracer = new Tracer({ - beforeMessage: this._beforeMessageHandler, - step: this._stepHandler, - afterMessage: this._afterMessageHandler, + beforeMessage: this._beforeMessageHandler.bind(this), + step: this._stepHandler.bind(this), + afterMessage: this._afterMessageHandler.bind(this), }); const rethnetResult = await this._rethnet.run( @@ -447,21 +447,21 @@ export class RethnetAdapter implements VMAdapter { return undefined; } - private _beforeMessageHandler = async ( + private async _beforeMessageHandler( message: TracingMessage, next: any - ) => { + ): Promise { await this._vmTracer.addBeforeMessage(message); - }; + } - private _stepHandler = async (step: TracingStep, _next: any) => { + private async _stepHandler(step: TracingStep, _next: any): Promise { await this._vmTracer.addStep(step); - }; + } - private _afterMessageHandler = async ( + private async _afterMessageHandler( result: TracingMessageResult, _next: any - ) => { + ): Promise { await this._vmTracer.addAfterMessage(result); - }; + } } diff --git a/packages/hardhat-core/test/internal/hardhat-network/provider/TxPool.ts b/packages/hardhat-core/test/internal/hardhat-network/provider/TxPool.ts index 8f8aba0dde..0f580f7e94 100644 --- a/packages/hardhat-core/test/internal/hardhat-network/provider/TxPool.ts +++ b/packages/hardhat-core/test/internal/hardhat-network/provider/TxPool.ts @@ -36,11 +36,7 @@ describe("Tx Pool", () => { beforeEach(() => { stateManager = new DefaultStateManager(); const common = new Common({ chain: "mainnet" }); - txPool = new TxPool( - (address) => stateManager.getAccount(address), - blockGasLimit, - common - ); + txPool = new TxPool(blockGasLimit, common); }); describe("addTransaction", () => { @@ -58,7 +54,10 @@ describe("Tx Pool", () => { from: address, nonce: 0, }); - await txPool.addTransaction(tx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ); const pendingTxs = txPool.getPendingTransactions(); assert.lengthOf(txMapToArray(pendingTxs), 1); @@ -76,7 +75,10 @@ describe("Tx Pool", () => { from: address, nonce: 3, }); - await txPool.addTransaction(tx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ); const pendingTxs = txPool.getPendingTransactions(); assert.equal(pendingTxs.size, 0); @@ -95,7 +97,10 @@ describe("Tx Pool", () => { }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ), Error, "Nonce too low" ); @@ -121,8 +126,14 @@ describe("Tx Pool", () => { from: address, nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); const pendingTxs = txPool.getPendingTransactions(); assert.sameDeepMembers( @@ -145,9 +156,18 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); const pendingTxs = txPool.getPendingTransactions(); assert.sameDeepMembers( @@ -174,10 +194,22 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); - await txPool.addTransaction(tx4); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); const pendingTxs = txPool.getPendingTransactions(); assert.sameDeepMembers( @@ -197,8 +229,14 @@ describe("Tx Pool", () => { from: address, nonce: 2, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); const pendingTxs = txPool.getPendingTransactions(); assert.sameDeepMembers( @@ -219,7 +257,10 @@ describe("Tx Pool", () => { nonce: 0, }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ), Error, "Nonce too low" ); @@ -242,8 +283,14 @@ describe("Tx Pool", () => { nonce: 0, gasPrice: 10, }); - await txPool.addTransaction(tx1a); - await txPool.addTransaction(tx1b); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1a + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1b + ); const pendingTxs = txPool.getPendingTransactions(); assert.sameDeepMembers( @@ -267,8 +314,14 @@ describe("Tx Pool", () => { nonce: 1, gasPrice: 10, }); - await txPool.addTransaction(tx2a); - await txPool.addTransaction(tx2b); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2a + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2b + ); const queuedTxs = txPool.getQueuedTransactions(); @@ -290,7 +343,10 @@ describe("Tx Pool", () => { gasPrice: 20, }); - await txPool.addTransaction(tx1a); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1a + ); const tx1b = createTestFakeTransaction({ from: address, @@ -299,7 +355,10 @@ describe("Tx Pool", () => { }); await assert.isRejected( - txPool.addTransaction(tx1b), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1b + ), InvalidInputError, `Replacement transaction underpriced. A gasPrice/maxFeePerGas of at least 22 is necessary to replace the existing transaction with nonce 0.` ); @@ -312,7 +371,10 @@ describe("Tx Pool", () => { }); await assert.isRejected( - txPool.addTransaction(tx1c), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1c + ), InvalidInputError, `Replacement transaction underpriced. A gasPrice/maxFeePerGas of at least 22 is necessary to replace the existing transaction with nonce 0.` ); @@ -325,7 +387,10 @@ describe("Tx Pool", () => { }); await assert.isRejected( - txPool.addTransaction(tx1d), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1d + ), InvalidInputError, `Replacement transaction underpriced. A gasPrice/maxPriorityFeePerGas of at least 22 is necessary to replace the existing transaction with nonce 0.` ); @@ -352,9 +417,15 @@ describe("Tx Pool", () => { nonce: 1, gasPrice: 21, }); - await txPool.addTransaction(tx2a); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2a + ); await assert.isRejected( - txPool.addTransaction(tx2b), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2b + ), InvalidInputError, `Replacement transaction underpriced. A gasPrice/maxFeePerGas of at least 22 is necessary to replace the existing transaction with nonce 1` ); @@ -395,8 +466,14 @@ describe("Tx Pool", () => { nonce: 0, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); const pendingTxs = txPool.getPendingTransactions(); @@ -425,10 +502,22 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); - await txPool.addTransaction(tx4); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); const pendingTxs = txPool.getPendingTransactions(); @@ -460,11 +549,26 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); - await txPool.addTransaction(tx4); - await txPool.addTransaction(tx5); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx5 + ); const pendingTxs = txPool.getPendingTransactions(); @@ -504,13 +608,34 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); - await txPool.addTransaction(tx4); - await txPool.addTransaction(tx5); - await txPool.addTransaction(tx6); - await txPool.addTransaction(tx7); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx5 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx6 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx7 + ); const pendingTxs = txPool.getPendingTransactions(); @@ -531,10 +656,16 @@ describe("Tx Pool", () => { const signedTx1 = tx1.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); const signedTx2 = tx2.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx1 + ); await assert.isRejected( - txPool.addTransaction(signedTx2), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx2 + ), InvalidInputError, `Known transaction: ${bufferToHex(signedTx1.hash())}` ); @@ -544,7 +675,7 @@ describe("Tx Pool", () => { const gasLimit = 15_000_000; const tx = createTestFakeTransaction({ gasLimit }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, `Transaction gas limit is ${gasLimit} and exceeds block gas limit of ${blockGasLimit}` ); @@ -553,7 +684,7 @@ describe("Tx Pool", () => { it("rejects if transaction is not signed", async () => { const tx = createUnsignedTestTransaction(); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, "Invalid Signature" ); @@ -572,7 +703,7 @@ describe("Tx Pool", () => { }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, "Nonce too low" ); @@ -582,7 +713,7 @@ describe("Tx Pool", () => { const gasLimit = 100; const tx = createTestFakeTransaction({ gasLimit }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, `Transaction requires at least 21000 gas but got ${gasLimit}` ); @@ -593,7 +724,7 @@ describe("Tx Pool", () => { to: undefined, }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, "contract creation without any data provided" ); @@ -616,7 +747,7 @@ describe("Tx Pool", () => { value: 5, }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, "sender doesn't have enough funds to send tx" ); @@ -628,7 +759,10 @@ describe("Tx Pool", () => { value: 5, }); await assert.isRejected( - txPool.addTransaction(tx2), + txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ), InvalidInputError, "sender doesn't have enough funds to send tx" ); @@ -667,10 +801,22 @@ describe("Tx Pool", () => { nonce: 0, }); - await txPool.addTransaction(txA.data); - await txPool.addTransaction(txB.data); - await txPool.addTransaction(txC.data); - await txPool.addTransaction(txD.data); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + txA.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + txB.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + txC.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + txD.data + ); const pendingTxs = txPool.getPendingTransactions(); assertEqualTransactionMaps( @@ -690,7 +836,10 @@ describe("Tx Pool", () => { gasLimit: 21_000, }); - await txPool.addTransaction(tx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ); const txFromTxPool = txPool.getTransactionByHash(tx.hash()); @@ -705,7 +854,10 @@ describe("Tx Pool", () => { gasLimit: 21_000, }); - await txPool.addTransaction(tx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ); const txFromTxPool = txPool.getTransactionByHash(tx.hash()); @@ -721,7 +873,10 @@ describe("Tx Pool", () => { const signedTx = tx.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx + ); const oldTxFromTxPool = txPool.getTransactionByHash(signedTx.hash()); @@ -735,7 +890,9 @@ describe("Tx Pool", () => { }) ); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const actualTxFromTxPool = txPool.getTransactionByHash(signedTx.hash()); @@ -751,7 +908,10 @@ describe("Tx Pool", () => { const signedTx = tx.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx + ); const oldTxFromTxPool = txPool.getTransactionByHash(signedTx.hash()); @@ -765,7 +925,9 @@ describe("Tx Pool", () => { }) ); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const actualTxFromTxPool = txPool.getTransactionByHash(signedTx.hash()); @@ -789,9 +951,17 @@ describe("Tx Pool", () => { nonce: 0, }); - await txPool.addTransaction(tx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); - assert.isTrue((await txPool.getNextPendingNonce(address)) === 1n); + assert.isTrue( + (await txPool.getNextPendingNonce( + stateManager.getAccount.bind(stateManager), + address + )) === 1n + ); }); it("is not affected by queued transactions", async () => { @@ -804,10 +974,21 @@ describe("Tx Pool", () => { nonce: 2, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); - assert.isTrue((await txPool.getNextPendingNonce(address)) === 1n); + assert.isTrue( + (await txPool.getNextPendingNonce( + stateManager.getAccount.bind(stateManager), + address + )) === 1n + ); }); it("returns correct nonce after all queued transactions are moved to pending", async () => { @@ -824,11 +1005,25 @@ describe("Tx Pool", () => { nonce: 1, }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); - assert.isTrue((await txPool.getNextPendingNonce(address)) === 3n); + assert.isTrue( + (await txPool.getNextPendingNonce( + stateManager.getAccount.bind(stateManager), + address + )) === 3n + ); }); it("returns correct nonce after some queued transactions are moved to pending", async () => { @@ -837,12 +1032,29 @@ describe("Tx Pool", () => { const tx3 = createTestFakeTransaction({ from: address, nonce: 5 }); const tx4 = createTestFakeTransaction({ from: address, nonce: 1 }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx3); - await txPool.addTransaction(tx4); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); - assert.isTrue((await txPool.getNextPendingNonce(address)) === 3n); + assert.isTrue( + (await txPool.getNextPendingNonce( + stateManager.getAccount.bind(stateManager), + address + )) === 3n + ); }); }); @@ -870,11 +1082,16 @@ describe("Tx Pool", () => { const tx1 = createTestTransaction({ nonce: 0, gasLimit: 9_500_000 }); const signedTx1 = tx1.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx1 + ); txPool.setBlockGasLimit(5_000_000); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const pendingTransactions = txPool.getPendingTransactions(); assertEqualTransactionMaps(pendingTransactions, makeOrderedTxMap([])); @@ -884,11 +1101,16 @@ describe("Tx Pool", () => { const tx1 = createTestTransaction({ nonce: 1, gasLimit: 9_500_000 }); const signedTx1 = tx1.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx1 + ); txPool.setBlockGasLimit(5_000_000); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const queuedTransactions = txPool.getQueuedTransactions(); assertEqualTransactionMaps(queuedTransactions, makeOrderedTxMap([])); @@ -920,10 +1142,22 @@ describe("Tx Pool", () => { from: address2, }); - await txPool.addTransaction(tx1.data); - await txPool.addTransaction(tx2.data); - await txPool.addTransaction(tx3.data); - await txPool.addTransaction(tx4.data); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3.data + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4.data + ); await stateManager.putAccount( address1, @@ -940,7 +1174,9 @@ describe("Tx Pool", () => { }) ); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const pendingTransactions = txPool.getPendingTransactions(); assertEqualTransactionMaps( @@ -957,14 +1193,19 @@ describe("Tx Pool", () => { }); const signedTx1 = tx1.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx1 + ); await stateManager.putAccount( address1, Account.fromAccountData({ nonce: 0n, balance: 0n }) ); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const pendingTransactions = txPool.getPendingTransactions(); assertEqualTransactionMaps(pendingTransactions, makeOrderedTxMap([])); @@ -978,14 +1219,19 @@ describe("Tx Pool", () => { }); const signedTx1 = tx1.sign(toBuffer(DEFAULT_ACCOUNTS[0].privateKey)); - await txPool.addTransaction(signedTx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + signedTx1 + ); await stateManager.putAccount( address1, Account.fromAccountData({ nonce: 0n, balance: 0n }) ); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const queuedTransactions = txPool.getQueuedTransactions(); assertEqualTransactionMaps(queuedTransactions, makeOrderedTxMap([])); @@ -1027,11 +1273,26 @@ describe("Tx Pool", () => { from: sender, }); - await txPool.addTransaction(tx0); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); - await txPool.addTransaction(tx4); - await txPool.addTransaction(tx5); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx0 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx4 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx5 + ); // pending: [0, 1, 2] // queued: [4, 5] @@ -1048,7 +1309,9 @@ describe("Tx Pool", () => { // this should drop tx1 txPool.setBlockGasLimit(150_000); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); // pending: [0] // queued: [2, 4, 5] @@ -1071,7 +1334,10 @@ describe("Tx Pool", () => { gasLimit: 100_000, from: sender, }); - await txPool.addTransaction(tx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); let pendingTxs = txPool.getPendingTransactions(); assert.lengthOf(txMapToArray(pendingTxs), 1); @@ -1081,14 +1347,19 @@ describe("Tx Pool", () => { assert.lengthOf(txMapToArray(queuedTxs), 0); txPool.setBlockGasLimit(90_000); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const tx2 = createTestFakeTransaction({ gasLimit: 80_000, from: sender, nonce: 0, }); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); pendingTxs = txPool.getPendingTransactions(); assert.lengthOf(txMapToArray(pendingTxs), 1); @@ -1121,9 +1392,18 @@ describe("Tx Pool", () => { from: sender, }); - await txPool.addTransaction(tx0); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx0 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); // pending: [0, 1, 2] // queued: [0] @@ -1138,13 +1418,18 @@ describe("Tx Pool", () => { // this should drop tx1 txPool.setBlockGasLimit(100_000); - await txPool.updatePendingAndQueued(); + await txPool.updatePendingAndQueued( + stateManager.getAccount.bind(stateManager) + ); const tx3 = createTestFakeTransaction({ nonce: 3, from: sender, }); - await txPool.addTransaction(tx3); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx3 + ); // pending: [0, 1, 2, 3] // queued: [] @@ -1177,7 +1462,7 @@ describe("Tx Pool", () => { txPool.setBlockGasLimit(21_000); const tx = createTestFakeTransaction({ gasLimit: 50_000 }); await assert.isRejected( - txPool.addTransaction(tx), + txPool.addTransaction(stateManager.getAccount.bind(stateManager), tx), InvalidInputError, "Transaction gas limit is 50000 and exceeds block gas limit of 21000" ); @@ -1193,7 +1478,10 @@ describe("Tx Pool", () => { it("returns a bigger snapshot id if the state changed", async () => { const id1 = txPool.snapshot(); const tx = createTestFakeTransaction(); - await txPool.addTransaction(tx); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx + ); const id2 = txPool.snapshot(); assert.isAbove(id2, id1); }); @@ -1219,7 +1507,10 @@ describe("Tx Pool", () => { orderId: 0, nonce: 0, }); - await txPool.addTransaction(tx1.data); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1.data + ); const id = txPool.snapshot(); @@ -1228,7 +1519,10 @@ describe("Tx Pool", () => { orderId: 1, nonce: 1, }); - await txPool.addTransaction(tx2.data); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2.data + ); txPool.revert(id); const pendingTransactions = txPool.getPendingTransactions(); @@ -1252,18 +1546,30 @@ describe("Tx Pool", () => { const tx1 = createTestFakeTransaction({ nonce: 0 }); const tx2 = createTestFakeTransaction({ nonce: 0 }); - await txPool.addTransaction(tx1); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); assert.isTrue(txPool.hasPendingTransactions()); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); assert.isTrue(txPool.hasPendingTransactions()); }); it("returns false when there are only queued transactions", async () => { const tx1 = createTestFakeTransaction({ nonce: 1 }); const tx2 = createTestFakeTransaction({ nonce: 1 }); - await txPool.addTransaction(tx1); - await txPool.addTransaction(tx2); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx1 + ); + await txPool.addTransaction( + stateManager.getAccount.bind(stateManager), + tx2 + ); assert.isFalse(txPool.hasPendingTransactions()); }); From 067c2679b520a5a4ae0d7285fe9ebdc447dee4c0 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Apr 2023 00:06:04 -0500 Subject: [PATCH 3/6] feat: add tracing instrumentation --- crates/rethnet_eth/src/remote.rs | 1 + crates/rethnet_evm/Cargo.toml | 4 + crates/rethnet_evm/src/block/builder.rs | 14 ++- crates/rethnet_evm/src/blockchain/sync.rs | 1 + crates/rethnet_evm/src/evm.rs | 12 +- crates/rethnet_evm/src/lib.rs | 1 + crates/rethnet_evm/src/runtime.rs | 19 ++- crates/rethnet_evm/src/state/debug.rs | 33 +++++- crates/rethnet_evm/src/state/layered_state.rs | 4 +- crates/rethnet_evm/src/state/remote.rs | 4 + crates/rethnet_evm/src/state/request.rs | 108 +----------------- crates/rethnet_evm/src/state/sync.rs | 47 ++++++-- crates/rethnet_evm_napi/Cargo.toml | 7 +- crates/rethnet_evm_napi/package.json | 1 + crates/rethnet_evm_napi/src/blockchain.rs | 8 +- crates/rethnet_evm_napi/src/lib.rs | 1 + crates/rethnet_evm_napi/src/logger.rs | 39 +++++++ crates/rethnet_evm_napi/src/runtime.rs | 17 +-- crates/rethnet_evm_napi/src/state.rs | 34 +++++- crates/rethnet_evm_napi/src/tracer.rs | 6 +- .../rethnet_evm_napi/src/tracer/js_tracer.rs | 17 ++- packages/hardhat-core/package.json | 2 + 22 files changed, 230 insertions(+), 150 deletions(-) create mode 100644 crates/rethnet_evm_napi/src/logger.rs diff --git a/crates/rethnet_eth/src/remote.rs b/crates/rethnet_eth/src/remote.rs index ac5a638367..5995b5a0d4 100644 --- a/crates/rethnet_eth/src/remote.rs +++ b/crates/rethnet_eth/src/remote.rs @@ -47,6 +47,7 @@ pub enum RpcClientError { } /// A client for executing RPC methods on a remote Ethereum node +#[derive(Debug)] pub struct RpcClient { url: String, client: reqwest::Client, diff --git a/crates/rethnet_evm/Cargo.toml b/crates/rethnet_evm/Cargo.toml index b85cfa0e66..04793616f0 100644 --- a/crates/rethnet_evm/Cargo.toml +++ b/crates/rethnet_evm/Cargo.toml @@ -13,6 +13,10 @@ revm = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = " # revm = { path = "../../../revm/crates/revm", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } thiserror = { version = "1.0.38", default-features = false } tokio = { version = "1.21.2", default-features = false, features = ["rt-multi-thread", "sync"] } +tracing = { version = "0.1.37", features = ["attributes", "std"], optional = true } [dev-dependencies] test-with = { version = "0.9.1", default-features = false } + +[features] +tracing = ["dep:tracing"] diff --git a/crates/rethnet_evm/src/block/builder.rs b/crates/rethnet_evm/src/block/builder.rs index 2afed9532e..31c6f2dcd1 100644 --- a/crates/rethnet_evm/src/block/builder.rs +++ b/crates/rethnet_evm/src/block/builder.rs @@ -7,13 +7,15 @@ use rethnet_eth::{ use revm::{ db::DatabaseComponentError, primitives::{BlockEnv, CfgEnv, EVMError, ExecutionResult, InvalidTransaction, SpecId, TxEnv}, - Inspector, }; use tokio::runtime::Runtime; use crate::{ - blockchain::AsyncBlockchain, evm::run_transaction, runtime::AsyncDatabase, state::AsyncState, - trace::Trace, HeaderData, + blockchain::AsyncBlockchain, + evm::{run_transaction, AsyncInspector}, + state::{AccountModifierFn, AsyncState}, + trace::Trace, + HeaderData, }; #[derive(Debug, thiserror::Error)] @@ -116,7 +118,7 @@ where pub async fn add_transaction( &mut self, transaction: TxEnv, - inspector: Option> + Send>>, + inspector: Option>>, ) -> Result<(ExecutionResult, Trace), BlockTransactionError> { // transaction's gas limit cannot be greater than the remaining gas in the block if U256::from(transaction.gas_limit) > self.gas_remaining() { @@ -165,7 +167,9 @@ where self.state .modify_account( address, - Box::new(move |balance, _nonce, _code| *balance += reward), + AccountModifierFn::new(Box::new(move |balance, _nonce, _code| { + *balance += reward; + })), ) .await?; } diff --git a/crates/rethnet_evm/src/blockchain/sync.rs b/crates/rethnet_evm/src/blockchain/sync.rs index dd7c8a8c35..4bef0acfeb 100644 --- a/crates/rethnet_evm/src/blockchain/sync.rs +++ b/crates/rethnet_evm/src/blockchain/sync.rs @@ -30,6 +30,7 @@ where /// A helper class for converting a synchronous blockchain into an asynchronous blockchain. /// /// Requires the inner blockchain to implement [`Blockchain`]. +#[derive(Debug)] pub struct AsyncBlockchain where E: Debug + Send, diff --git a/crates/rethnet_evm/src/evm.rs b/crates/rethnet_evm/src/evm.rs index 572a2c5130..9cf6eadbff 100644 --- a/crates/rethnet_evm/src/evm.rs +++ b/crates/rethnet_evm/src/evm.rs @@ -15,8 +15,17 @@ use crate::{ trace::{Trace, TraceCollector}, }; +/// Super trait for an inspector of an `AsyncDatabase` that's debuggable. +pub trait AsyncInspector: Inspector> + Debug + Send +where + BE: Debug + Send + 'static, + SE: Debug + Send + 'static, +{ +} + /// Creates an evm from the provided database, config, transaction, and block. #[allow(clippy::type_complexity)] +#[cfg_attr(feature = "tracing", tracing::instrument)] fn build_evm( blockchain: Arc>, state: Arc>, @@ -41,6 +50,7 @@ where } #[allow(clippy::type_complexity)] +#[cfg_attr(feature = "tracing", tracing::instrument)] pub fn run_transaction( runtime: &Runtime, blockchain: Arc>, @@ -48,7 +58,7 @@ pub fn run_transaction( cfg: CfgEnv, transaction: TxEnv, block: BlockEnv, - inspector: Option> + Send>>, + inspector: Option>>, ) -> JoinHandle>>> where BE: Debug + Send + 'static, diff --git a/crates/rethnet_evm/src/lib.rs b/crates/rethnet_evm/src/lib.rs index ff2167a5d3..d89b9fb640 100644 --- a/crates/rethnet_evm/src/lib.rs +++ b/crates/rethnet_evm/src/lib.rs @@ -21,6 +21,7 @@ pub use revm::{ pub use crate::{ block::{BlockBuilder, HeaderData}, + evm::AsyncInspector, runtime::{AsyncDatabase, Rethnet}, transaction::{PendingTransaction, TransactionError}, }; diff --git a/crates/rethnet_evm/src/runtime.rs b/crates/rethnet_evm/src/runtime.rs index 41d5752f0d..a135fada88 100644 --- a/crates/rethnet_evm/src/runtime.rs +++ b/crates/rethnet_evm/src/runtime.rs @@ -3,18 +3,22 @@ use std::{fmt::Debug, sync::Arc}; use revm::{ db::DatabaseComponents, primitives::{BlockEnv, CfgEnv, ExecutionResult, SpecId, TxEnv}, - Inspector, }; use crate::{ - blockchain::AsyncBlockchain, evm::run_transaction, state::AsyncState, trace::Trace, - transaction::TransactionError, State, + blockchain::AsyncBlockchain, + evm::{run_transaction, AsyncInspector}, + state::AsyncState, + trace::Trace, + transaction::TransactionError, + State, }; /// Asynchronous implementation of the Database super-trait pub type AsyncDatabase = DatabaseComponents>, Arc>>; /// The asynchronous Rethnet runtime. +#[derive(Debug)] pub struct Rethnet where BE: Debug + Send + 'static, @@ -40,11 +44,12 @@ where } /// Runs a transaction without committing the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn dry_run( &self, transaction: TxEnv, block: BlockEnv, - inspector: Option> + Send>>, + inspector: Option>>, ) -> Result<(ExecutionResult, State, Trace), TransactionError> { if self.cfg.spec_id > SpecId::MERGE && block.prevrandao.is_none() { return Err(TransactionError::MissingPrevrandao); @@ -65,11 +70,12 @@ where } /// Runs a transaction without committing the state, while disabling balance checks and creating accounts for new addresses. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn guaranteed_dry_run( &self, transaction: TxEnv, block: BlockEnv, - inspector: Option> + Send>>, + inspector: Option>>, ) -> Result<(ExecutionResult, State, Trace), TransactionError> { if self.cfg.spec_id > SpecId::MERGE && block.prevrandao.is_none() { return Err(TransactionError::MissingPrevrandao); @@ -93,11 +99,12 @@ where } /// Runs a transaction, committing the state in the process. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn run( &self, transaction: TxEnv, block: BlockEnv, - inspector: Option> + Send>>, + inspector: Option>>, ) -> Result<(ExecutionResult, Trace), TransactionError> { let (result, changes, trace) = self.dry_run(transaction, block, inspector).await?; diff --git a/crates/rethnet_evm/src/state/debug.rs b/crates/rethnet_evm/src/state/debug.rs index ecaa084d82..bf1f9aec87 100644 --- a/crates/rethnet_evm/src/state/debug.rs +++ b/crates/rethnet_evm/src/state/debug.rs @@ -1,9 +1,38 @@ +use std::{fmt::Debug, ops::Deref}; + use auto_impl::auto_impl; use rethnet_eth::{Address, B256, U256}; use revm::primitives::{AccountInfo, Bytecode}; -/// Function type for modifying account information. -pub type AccountModifierFn = Box) + Send>; +/// Debuggable function type for modifying account information. +pub struct AccountModifierFn { + inner: Box) + Send>, +} + +impl AccountModifierFn { + /// Constructs an [`AccountModifierDebuggableFn`] from the provided function. + pub fn new(func: Box) + Send>) -> Self { + Self { inner: func } + } +} + +impl Debug for AccountModifierFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + std::any::type_name::)>() + ) + } +} + +impl Deref for AccountModifierFn { + type Target = dyn Fn(&mut U256, &mut u64, &mut Option); + + fn deref(&self) -> &Self::Target { + self.inner.as_ref() + } +} /// A trait for debug operation on a database. #[auto_impl(Box)] diff --git a/crates/rethnet_evm/src/state/layered_state.rs b/crates/rethnet_evm/src/state/layered_state.rs index 2b815f869e..b6a0f8564a 100644 --- a/crates/rethnet_evm/src/state/layered_state.rs +++ b/crates/rethnet_evm/src/state/layered_state.rs @@ -11,7 +11,7 @@ use revm::{ DatabaseCommit, }; -use super::{StateDebug, StateError}; +use super::{AccountModifierFn, StateDebug, StateError}; /// A state consisting of layers. #[derive(Clone, Debug)] @@ -356,7 +356,7 @@ impl StateDebug for LayeredState { fn modify_account( &mut self, address: Address, - modifier: Box) + Send>, + modifier: AccountModifierFn, ) -> Result<(), Self::Error> { let account_info = self.account_or_insert_mut(&address); let old_code_hash = account_info.code_hash; diff --git a/crates/rethnet_evm/src/state/remote.rs b/crates/rethnet_evm/src/state/remote.rs index 06c0b420bc..b8da9b5dba 100644 --- a/crates/rethnet_evm/src/state/remote.rs +++ b/crates/rethnet_evm/src/state/remote.rs @@ -10,6 +10,7 @@ use rethnet_eth::{ }; /// An revm database backed by a remote Ethereum node +#[derive(Debug)] pub struct RemoteDatabase { client: RpcClient, runtime: Runtime, @@ -28,6 +29,7 @@ pub enum RemoteDatabaseError { impl RemoteDatabase { /// Construct a new RemoteDatabse given the URL of a remote Ethereum node. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub fn new(url: &str) -> Self { Self { client: RpcClient::new(url), @@ -51,6 +53,7 @@ impl RemoteDatabase { impl StateRef for RemoteDatabase { type Error = RemoteDatabaseError; + #[cfg_attr(feature = "tracing", tracing::instrument)] fn basic(&self, address: Address) -> Result, Self::Error> { Ok(Some( self.runtime @@ -64,6 +67,7 @@ impl StateRef for RemoteDatabase { unimplemented!(); } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn storage(&self, address: Address, index: U256) -> Result { self.runtime .block_on(self.client.get_storage_at(&address, index, None)) diff --git a/crates/rethnet_evm/src/state/request.rs b/crates/rethnet_evm/src/state/request.rs index f6a1c6f8ed..6a1fe0bac1 100644 --- a/crates/rethnet_evm/src/state/request.rs +++ b/crates/rethnet_evm/src/state/request.rs @@ -12,10 +12,8 @@ use tokio::sync::oneshot; use crate::state::{AccountModifierFn, StateDebug}; /// The request type used internally by a [`SyncDatabase`]. -pub enum Request -where - E: Debug, -{ +#[derive(Debug)] +pub enum Request { AccountByAddress { address: Address, sender: oneshot::Sender, E>>, @@ -84,9 +82,10 @@ impl Request where E: Debug, { + #[cfg_attr(feature = "tracing", tracing::instrument)] pub fn handle(self, state: &mut S) -> bool where - S: State + DatabaseCommit + StateDebug, + S: State + DatabaseCommit + StateDebug + Debug, { match self { Request::AccountByAddress { address, sender } => { @@ -148,102 +147,3 @@ where true } } - -impl Debug for Request -where - E: Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::AccountByAddress { address, sender } => f - .debug_struct("AccountByAddress") - .field("address", address) - .field("sender", sender) - .finish(), - Self::AccountStorageRoot { address, sender } => f - .debug_struct("AccountStorageRoot") - .field("address", address) - .field("sender", sender) - .finish(), - Self::Checkpoint { sender } => f - .debug_struct("Checkpoint") - .field("sender", sender) - .finish(), - Self::CodeByHash { code_hash, sender } => f - .debug_struct("CodeByHash") - .field("code_hash", code_hash) - .field("sender", sender) - .finish(), - Self::Commit { changes, sender } => f - .debug_struct("Commit") - .field("changes", changes) - .field("sender", sender) - .finish(), - Self::InsertAccount { - address, - account_info, - sender, - } => f - .debug_struct("InsertAccount") - .field("address", address) - .field("account_info", account_info) - .field("sender", sender) - .finish(), - Self::MakeSnapshot { sender } => f - .debug_struct("MakeSnapshot") - .field("sender", sender) - .finish(), - Self::ModifyAccount { - address, - modifier: _modifier, - sender, - } => f - .debug_struct("ModifyAccount") - .field("address", address) - .field("sender", sender) - .finish(), - Self::RemoveAccount { address, sender } => f - .debug_struct("RemoveAccount") - .field("address", address) - .field("sender", sender) - .finish(), - Self::RemoveSnapshot { state_root, sender } => f - .debug_struct("RemoveSnapshot") - .field("state_root", state_root) - .field("sender", sender) - .finish(), - Self::Revert { sender } => f.debug_struct("Revert").field("sender", sender).finish(), - Self::SetStorageSlot { - address, - index, - value, - sender, - } => f - .debug_struct("SetStorageSlot") - .field("address", address) - .field("index", index) - .field("value", value) - .field("sender", sender) - .finish(), - Self::SetStateRoot { state_root, sender } => f - .debug_struct("SetStateRoot") - .field("state_root", state_root) - .field("sender", sender) - .finish(), - Self::StateRoot { sender } => { - f.debug_struct("StateRoot").field("sender", sender).finish() - } - Self::StorageSlot { - address, - index, - sender, - } => f - .debug_struct("StorageSlot") - .field("address", address) - .field("index", index) - .field("sender", sender) - .finish(), - Self::Terminate => write!(f, "Terminate"), - } - } -} diff --git a/crates/rethnet_evm/src/state/sync.rs b/crates/rethnet_evm/src/state/sync.rs index f47381e34c..1b09e331df 100644 --- a/crates/rethnet_evm/src/state/sync.rs +++ b/crates/rethnet_evm/src/state/sync.rs @@ -22,7 +22,7 @@ use super::request::Request; /// Trait that meets all requirements for a synchronous database that can be used by [`AsyncDatabase`]. pub trait SyncState: - State + DatabaseCommit + StateDebug + Send + Sync + 'static + State + DatabaseCommit + StateDebug + Debug + Send + Sync + 'static where E: Debug + Send, { @@ -30,7 +30,7 @@ where impl SyncState for S where - S: State + DatabaseCommit + StateDebug + Send + Sync + 'static, + S: State + DatabaseCommit + StateDebug + Debug + Send + Sync + 'static, E: Debug + Send, { } @@ -38,6 +38,8 @@ where /// A helper class for converting a synchronous database into an asynchronous database. /// /// Requires the inner database to implement [`Database`], [`DatabaseCommit`], and [`DatabaseDebug`]. + +#[derive(Debug)] pub struct AsyncState where E: Debug + Send, @@ -52,6 +54,7 @@ where E: Debug + Send + 'static, { /// Constructs an [`AsyncDatabase`] instance with the provided database. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub fn new>(mut state: S) -> io::Result { let runtime = Builder::new_multi_thread().build()?; @@ -78,6 +81,7 @@ where } /// Retrieves the account corresponding to the specified address. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn account_by_address(&self, address: Address) -> Result, E> { let (sender, receiver) = oneshot::channel(); @@ -89,6 +93,7 @@ where } /// Retrieves the storage root of the account at the specified address. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn account_storage_root(&self, address: &Address) -> Result, E> { let (sender, receiver) = oneshot::channel(); @@ -103,6 +108,7 @@ where } /// Retrieves the storage slot corresponding to the specified address and index. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn account_storage_slot(&self, address: Address, index: U256) -> Result { let (sender, receiver) = oneshot::channel(); @@ -118,6 +124,7 @@ where } /// Applies the provided changes to the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn apply(&self, changes: HashMap) { let (sender, receiver) = oneshot::channel(); @@ -129,6 +136,7 @@ where } /// Creates a state checkpoint that can be reverted to using [`revert`]. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn checkpoint(&self) -> Result<(), E> { let (sender, receiver) = oneshot::channel(); @@ -140,6 +148,7 @@ where } /// Retrieves the code corresponding to the specified hash. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn code_by_hash(&self, code_hash: B256) -> Result { let (sender, receiver) = oneshot::channel(); @@ -151,6 +160,7 @@ where } /// Inserts the specified account into the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn insert_account( &self, address: Address, @@ -170,6 +180,7 @@ where } /// Makes a snapshot of the database that's retained until [`remove_snapshot`] is called. Returns the snapshot's identifier. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn make_snapshot(&self) -> (B256, bool) { let (sender, receiver) = oneshot::channel(); @@ -181,6 +192,7 @@ where } /// Modifies the account at the specified address using the provided function. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn modify_account( &self, address: Address, @@ -200,6 +212,7 @@ where } /// Removes and returns the account at the specified address, if it exists. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn remove_account(&self, address: Address) -> Result, E> { let (sender, receiver) = oneshot::channel(); @@ -211,6 +224,7 @@ where } /// Removes the snapshot corresponding to the specified id, if it exists. Returns whether a snapshot was removed. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn remove_snapshot(&self, state_root: B256) -> bool { let (sender, receiver) = oneshot::channel(); @@ -222,6 +236,7 @@ where } /// Reverts to the previous checkpoint, created using [`checkpoint`]. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn revert(&self) -> Result<(), E> { let (sender, receiver) = oneshot::channel(); @@ -233,6 +248,7 @@ where } /// Sets the storage slot at the specified address and index to the provided value. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn set_account_storage_slot( &self, address: Address, @@ -254,6 +270,7 @@ where } /// Reverts the state to match the specified state root. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn set_state_root(&self, state_root: &B256) -> Result<(), E> { let (sender, receiver) = oneshot::channel(); @@ -268,6 +285,7 @@ where } /// Retrieves the state's root. + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn state_root(&self) -> Result { let (sender, receiver) = oneshot::channel(); @@ -283,6 +301,7 @@ impl Drop for AsyncState where E: Debug + Send, { + #[cfg_attr(feature = "tracing", tracing::instrument)] fn drop(&mut self) { if let Some(handle) = self.db_handle.take() { self.request_sender @@ -300,6 +319,7 @@ where { type Error = E; + #[cfg_attr(feature = "tracing", tracing::instrument)] fn basic(&self, address: Address) -> Result, Self::Error> { task::block_in_place(move || { self.runtime @@ -307,6 +327,7 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn code_by_hash(&self, code_hash: B256) -> Result { task::block_in_place(move || { self.runtime @@ -314,6 +335,7 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn storage(&self, address: Address, index: U256) -> Result { task::block_in_place(move || { self.runtime @@ -326,6 +348,7 @@ impl<'d, E> DatabaseCommit for &'d AsyncState where E: Debug + Send + 'static, { + #[cfg_attr(feature = "tracing", tracing::instrument)] fn commit(&mut self, changes: HashMap) { task::block_in_place(move || self.runtime.block_on(self.apply(changes))) } @@ -337,6 +360,7 @@ where { type Error = E; + #[cfg_attr(feature = "tracing", tracing::instrument)] fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { task::block_in_place(move || { self.runtime @@ -344,6 +368,7 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn insert_account( &mut self, address: Address, @@ -355,10 +380,11 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn modify_account( &mut self, address: Address, - modifier: Box) + Send>, + modifier: AccountModifierFn, ) -> Result<(), Self::Error> { task::block_in_place(move || { self.runtime @@ -366,6 +392,7 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn remove_account(&mut self, address: Address) -> Result, Self::Error> { task::block_in_place(move || { self.runtime @@ -386,6 +413,12 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn state_root(&mut self) -> Result { + task::block_in_place(move || self.runtime.block_on(AsyncState::state_root(*self))) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { task::block_in_place(move || { self.runtime @@ -393,22 +426,22 @@ where }) } - fn state_root(&mut self) -> Result { - task::block_in_place(move || self.runtime.block_on(AsyncState::state_root(*self))) - } - + #[cfg_attr(feature = "tracing", tracing::instrument)] fn checkpoint(&mut self) -> Result<(), Self::Error> { task::block_in_place(move || self.runtime.block_on(AsyncState::checkpoint(*self))) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn revert(&mut self) -> Result<(), Self::Error> { task::block_in_place(move || self.runtime.block_on(AsyncState::revert(*self))) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn make_snapshot(&mut self) -> (B256, bool) { task::block_in_place(move || self.runtime.block_on(AsyncState::make_snapshot(*self))) } + #[cfg_attr(feature = "tracing", tracing::instrument)] fn remove_snapshot(&mut self, state_root: &B256) -> bool { task::block_in_place(move || { self.runtime diff --git a/crates/rethnet_evm_napi/Cargo.toml b/crates/rethnet_evm_napi/Cargo.toml index ef01898c36..e05efaf300 100644 --- a/crates/rethnet_evm_napi/Cargo.toml +++ b/crates/rethnet_evm_napi/Cargo.toml @@ -12,11 +12,16 @@ crossbeam-channel = { version = "0.5.6", default-features = false } napi = { version = "2.12.4", default-features = false, features = ["async", "error_anyhow", "napi8", "serde-json"] } napi-derive = "2.12.3" once_cell = "1.15.0" -pretty_env_logger = "0.4.0" rethnet_evm = { version = "0.1.0-dev", path = "../rethnet_evm" } rethnet_eth = { version = "0.1.0-dev", path = "../rethnet_eth" } secp256k1 = { version = "0.24.0", default-features = false, features = ["alloc"] } serde_json = { version = "1.0.85", default-features = false, features = ["alloc"] } +tracing = { version = "0.1.37", default-features = false, features = ["std"] } +tracing-flame = { version = "0.2.0", default-features = false, features = ["smallvec"] } +tracing-subscriber = { version = "0.3.16", default-features = false, features = ["ansi", "env-filter", "fmt", "parking_lot", "smallvec", "std"] } [build-dependencies] napi-build = "2.0.1" + +[features] +tracing = ["rethnet_evm/tracing"] diff --git a/crates/rethnet_evm_napi/package.json b/crates/rethnet_evm_napi/package.json index 1b360dc4c2..f47230e241 100644 --- a/crates/rethnet_evm_napi/package.json +++ b/crates/rethnet_evm_napi/package.json @@ -11,6 +11,7 @@ "scripts": { "build": "napi build --release", "build:debug": "napi build", + "build:tracing": "napi build --release --features tracing", "test": "yarn tsc && mocha --recursive \"test/**/*.ts\" --exit", "clean": "rm -rf rethnet-evm.node" }, diff --git a/crates/rethnet_evm_napi/src/blockchain.rs b/crates/rethnet_evm_napi/src/blockchain.rs index 4ddea105aa..cec1916abc 100644 --- a/crates/rethnet_evm_napi/src/blockchain.rs +++ b/crates/rethnet_evm_napi/src/blockchain.rs @@ -1,6 +1,6 @@ mod js_blockchain; -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use napi::{bindgen_prelude::Buffer, Env, JsFunction, NapiRaw, Status}; use napi_derive::napi; @@ -8,6 +8,7 @@ use rethnet_eth::B256; use rethnet_evm::blockchain::{AsyncBlockchain, SyncBlockchain}; use crate::{ + logger::enable_logging, sync::{await_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}, }; @@ -16,6 +17,7 @@ use self::js_blockchain::{GetBlockHashCall, JsBlockchain}; /// The Rethnet blockchain #[napi] +#[derive(Debug)] pub struct Blockchain { inner: Arc>, } @@ -31,11 +33,14 @@ impl Blockchain { impl Blockchain { /// Constructs a new blockchain that queries the blockhash using a callback. #[napi(constructor)] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn new( env: Env, #[napi(ts_arg_type = "(blockNumber: bigint) => Promise")] get_block_hash_fn: JsFunction, ) -> napi::Result { + enable_logging(); + let get_block_hash_fn = ThreadsafeFunction::create( env.raw(), unsafe { get_block_hash_fn.raw() }, @@ -57,6 +62,7 @@ impl Blockchain { Self::with_blockchain(JsBlockchain { get_block_hash_fn }) } + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn with_blockchain(blockchain: B) -> napi::Result where B: SyncBlockchain, diff --git a/crates/rethnet_evm_napi/src/lib.rs b/crates/rethnet_evm_napi/src/lib.rs index fd188fdd7a..be2f80bc41 100644 --- a/crates/rethnet_evm_napi/src/lib.rs +++ b/crates/rethnet_evm_napi/src/lib.rs @@ -8,6 +8,7 @@ mod blockchain; mod cast; mod config; mod log; +mod logger; mod receipt; /// Rethnet runtime for executing individual transactions mod runtime; diff --git a/crates/rethnet_evm_napi/src/logger.rs b/crates/rethnet_evm_napi/src/logger.rs new file mode 100644 index 0000000000..af5da362c8 --- /dev/null +++ b/crates/rethnet_evm_napi/src/logger.rs @@ -0,0 +1,39 @@ +use once_cell::sync::OnceCell; +use tracing_subscriber::{prelude::*, EnvFilter, Registry}; + +struct Logger { + #[cfg(feature = "tracing")] + _guard: tracing_flame::FlushGuard>, +} + +unsafe impl Sync for Logger {} + +static LOGGER: OnceCell = OnceCell::new(); + +pub fn enable_logging() { + let _logger = LOGGER.get_or_init(|| { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_target(false) + .with_level(true) + .with_filter(EnvFilter::from_default_env()); + + #[cfg(feature = "tracing")] + let (flame_layer, _guard) = tracing_flame::FlameLayer::with_file("tracing.folded").unwrap(); + + let subscriber = Registry::default().with(fmt_layer); + + #[cfg(feature = "tracing")] + let subscriber = subscriber.with(flame_layer); + + tracing::subscriber::set_global_default(subscriber) + .expect("Could not set global default tracing subscriber"); + + Logger { + #[cfg(feature = "tracing")] + _guard, + } + }); +} diff --git a/crates/rethnet_evm_napi/src/runtime.rs b/crates/rethnet_evm_napi/src/runtime.rs index ebd0f896a4..472ae5614e 100644 --- a/crates/rethnet_evm_napi/src/runtime.rs +++ b/crates/rethnet_evm_napi/src/runtime.rs @@ -1,6 +1,5 @@ use napi::Status; use napi_derive::napi; -use once_cell::sync::OnceCell; use rethnet_evm::{ state::StateError, BlockEnv, CfgEnv, InvalidTransaction, TransactionError, TxEnv, }; @@ -14,14 +13,9 @@ use crate::{ transaction::{result::ExecutionResult, Transaction}, }; -struct Logger; - -unsafe impl Sync for Logger {} - -static LOGGER: OnceCell = OnceCell::new(); - /// The Rethnet runtime, which can execute individual transactions. #[napi] +#[derive(Debug)] pub struct Rethnet { runtime: rethnet_evm::Rethnet, } @@ -30,16 +24,12 @@ pub struct Rethnet { impl Rethnet { /// Constructs a `Rethnet` runtime. #[napi(constructor)] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn new( blockchain: &Blockchain, state_manager: &StateManager, cfg: Config, ) -> napi::Result { - let _logger = LOGGER.get_or_init(|| { - pretty_env_logger::init(); - Logger - }); - let cfg = CfgEnv::try_from(cfg)?; let runtime = rethnet_evm::Rethnet::new( @@ -53,6 +43,7 @@ impl Rethnet { /// Executes the provided transaction without changing state. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn dry_run( &self, transaction: Transaction, @@ -75,6 +66,7 @@ impl Rethnet { /// Executes the provided transaction without changing state, ignoring validation checks in the process. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn guaranteed_dry_run( &self, transaction: Transaction, @@ -97,6 +89,7 @@ impl Rethnet { /// Executes the provided transaction, changing state in the process. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn run( &self, transaction: Transaction, diff --git a/crates/rethnet_evm_napi/src/state.rs b/crates/rethnet_evm_napi/src/state.rs index 9b2c10278d..359943838d 100644 --- a/crates/rethnet_evm_napi/src/state.rs +++ b/crates/rethnet_evm_napi/src/state.rs @@ -10,7 +10,10 @@ use napi::{bindgen_prelude::*, JsFunction, JsObject, NapiRaw, Status}; use napi_derive::napi; use rethnet_eth::{signature::private_key_to_address, Address, Bytes, B256, U256}; use rethnet_evm::{ - state::{AsyncState, LayeredState, RethnetLayer, StateDebug, StateError, SyncState}, + state::{ + AccountModifierFn, AsyncState, LayeredState, RethnetLayer, StateDebug, StateError, + SyncState, + }, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY, }; use secp256k1::Secp256k1; @@ -18,6 +21,7 @@ use secp256k1::Secp256k1; use crate::{ account::Account, cast::TryCast, + logger::enable_logging, sync::{await_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; @@ -49,6 +53,7 @@ pub struct SnapshotId { /// The Rethnet state #[napi] +#[derive(Debug)] pub struct StateManager { pub(super) state: Arc>, } @@ -57,12 +62,14 @@ pub struct StateManager { impl StateManager { /// Constructs a [`StateManager`] with an empty state. #[napi(constructor)] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn new() -> napi::Result { Self::with_accounts(HashMap::default()) } /// Constructs a [`StateManager`] with the provided accounts present in the genesis state. #[napi(factory)] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn with_genesis_accounts(accounts: Vec) -> napi::Result { let context = Secp256k1::signing_only(); let genesis_accounts = accounts @@ -84,6 +91,7 @@ impl StateManager { Self::with_accounts(genesis_accounts) } + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn with_accounts(mut accounts: HashMap) -> napi::Result { // Mimic precompiles activation for idx in 1..=8 { @@ -99,10 +107,13 @@ impl StateManager { Self::with_state(state) } + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn with_state(state: S) -> napi::Result where S: SyncState, { + enable_logging(); + let state: Box> = Box::new(state); let state = AsyncState::new(state) .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; @@ -114,15 +125,19 @@ impl StateManager { /// Creates a state checkpoint that can be reverted to using [`revert`]. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn checkpoint(&self) -> napi::Result<()> { self.state .checkpoint() .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) + .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; + + Ok(()) } /// Reverts to the previous checkpoint, created using [`checkpoint`]. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn revert(&self) -> napi::Result<()> { self.state .revert() @@ -132,6 +147,7 @@ impl StateManager { /// Retrieves the account corresponding to the specified address. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_account_by_address(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); @@ -157,6 +173,7 @@ impl StateManager { /// Retrieves the storage root of the account at the specified address. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_account_storage_root(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); @@ -168,6 +185,7 @@ impl StateManager { /// Retrieves the storage slot at the specified address and index. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_account_storage_slot( &self, address: Buffer, @@ -192,6 +210,7 @@ impl StateManager { /// Retrieves the storage root of the database. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get_state_root(&self) -> napi::Result { self.state.state_root().await.map_or_else( |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), @@ -201,6 +220,7 @@ impl StateManager { /// Inserts the provided account at the specified address. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn insert_account(&self, address: Buffer, account: Account) -> napi::Result<()> { let address = Address::from_slice(&address); let account: AccountInfo = account.try_cast()?; @@ -213,6 +233,7 @@ impl StateManager { /// Makes a snapshot of the database that's retained until [`removeSnapshot`] is called. Returns the snapshot's identifier. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn make_snapshot(&self) -> SnapshotId { let (state_root, existed) = self.state.make_snapshot().await; @@ -226,6 +247,7 @@ impl StateManager { /// The modifier function receives the current values as individual parameters and will update the account's values /// to the returned `Account` values. #[napi(ts_return_type = "Promise")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn modify_account( &self, env: Env, @@ -302,7 +324,7 @@ impl StateManager { let result = db .modify_account( address, - Box::new( + AccountModifierFn::new(Box::new( move |balance: &mut U256, nonce: &mut u64, code: &mut Option| { let (sender, receiver) = channel(); @@ -323,7 +345,7 @@ impl StateManager { *nonce = new_account.nonce; *code = new_account.code; }, - ), + )), ) .await .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())); @@ -336,6 +358,7 @@ impl StateManager { /// Removes and returns the account at the specified address, if it exists. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn remove_account(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); @@ -347,6 +370,7 @@ impl StateManager { /// Removes the snapshot corresponding to the specified state root, if it exists. Returns whether a snapshot was removed. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn remove_snapshot(&self, state_root: Buffer) -> bool { let state_root = B256::from_slice(&state_root); @@ -355,6 +379,7 @@ impl StateManager { /// Sets the storage slot at the specified address and index to the provided value. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn set_account_storage_slot( &self, address: Buffer, @@ -373,6 +398,7 @@ impl StateManager { /// Reverts the state to match the specified state root. #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn set_state_root(&self, state_root: Buffer) -> napi::Result<()> { let state_root = B256::from_slice(&state_root); diff --git a/crates/rethnet_evm_napi/src/tracer.rs b/crates/rethnet_evm_napi/src/tracer.rs index 32be8cf5c8..1de31c2d8a 100644 --- a/crates/rethnet_evm_napi/src/tracer.rs +++ b/crates/rethnet_evm_napi/src/tracer.rs @@ -2,7 +2,7 @@ mod js_tracer; use napi::Env; use napi_derive::napi; -use rethnet_evm::{state::StateError, AsyncDatabase, Inspector}; +use rethnet_evm::{state::StateError, AsyncInspector}; use self::js_tracer::{JsTracer, TracingCallbacks}; @@ -12,9 +12,7 @@ pub struct Tracer { } impl Tracer { - pub fn as_dyn_inspector( - &self, - ) -> Box<(dyn Inspector> + Send + 'static)> { + pub fn as_dyn_inspector(&self) -> Box> { self.inner.clone() } } diff --git a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs index 005e3ea183..21a3459b21 100644 --- a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs +++ b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs @@ -10,7 +10,9 @@ use napi::{ }; use napi_derive::napi; use rethnet_eth::{Address, Bytes, U256}; -use rethnet_evm::{opcode, return_revert, Bytecode, Gas, InstructionResult, SuccessOrHalt}; +use rethnet_evm::{ + opcode, return_revert, AsyncInspector, Bytecode, Gas, InstructionResult, SuccessOrHalt, +}; use crate::{ sync::{await_void_promise, handle_error}, @@ -594,6 +596,19 @@ impl JsTracer { } } +impl Debug for JsTracer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsTracer").finish() + } +} + +impl AsyncInspector for JsTracer +where + BE: Debug + Send + 'static, + SE: Debug + Send + 'static, +{ +} + impl rethnet_evm::Inspector for JsTracer where D: rethnet_evm::Database, diff --git a/packages/hardhat-core/package.json b/packages/hardhat-core/package.json index 23f149860d..3391f4b295 100644 --- a/packages/hardhat-core/package.json +++ b/packages/hardhat-core/package.json @@ -33,7 +33,9 @@ "test:tracing": "mocha --recursive \"test/internal/hardhat-network/{helpers,stack-traces}/**/*.ts\"", "test:forking": "mocha --recursive \"test/internal/hardhat-network/{helpers,jsonrpc,provider}/**/*.ts\"", "prebuild": "cd ../../crates/rethnet_evm_napi && yarn build", + "prebuild:tracing": "cd ../../crates/rethnet_evm_napi && yarn build:tracing", "build": "tsc --build .", + "build:tracing": "tsc --build .", "prepublishOnly": "yarn build", "clean": "rimraf builtin-tasks internal types utils *.d.ts *.map *.js build-test tsconfig.tsbuildinfo test/internal/hardhat-network/provider/.hardhat_node_test_cache" }, From 7882bd2355a0b6838952adcd2b55b4a9ba479b74 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Apr 2023 00:07:05 -0500 Subject: [PATCH 4/6] feat: add hybrid state management Hybrid state management uses a Merkle-Patricia trie for the latest state and layers for history. --- crates/rethnet_eth/Cargo.toml | 1 - crates/rethnet_eth/src/account.rs | 34 +- crates/rethnet_eth/src/state.rs | 14 +- crates/rethnet_evm/Cargo.toml | 5 + crates/rethnet_evm/src/state.rs | 11 +- crates/rethnet_evm/src/state/account.rs | 17 + crates/rethnet_evm/src/state/contract.rs | 118 ++++ crates/rethnet_evm/src/state/debug.rs | 25 +- crates/rethnet_evm/src/state/history.rs | 25 + crates/rethnet_evm/src/state/hybrid.rs | 277 ++++++++ crates/rethnet_evm/src/state/layered.rs | 277 ++++++++ .../rethnet_evm/src/state/layered/changes.rs | 329 +++++++++ crates/rethnet_evm/src/state/layered_state.rs | 515 -------------- crates/rethnet_evm/src/state/request.rs | 14 +- crates/rethnet_evm/src/state/sync.rs | 47 +- crates/rethnet_evm/src/state/trie.rs | 234 +++++++ crates/rethnet_evm/src/state/trie/account.rs | 658 ++++++++++++++++++ crates/rethnet_evm_napi/src/state.rs | 14 +- .../hardhat-network/provider/RethnetState.ts | 4 + .../provider/fork/ForkStateManager.ts | 9 + .../hardhat-network/provider/vm/dual.ts | 9 + .../hardhat-network/provider/vm/ethereumjs.ts | 146 +++- .../hardhat-network/provider/vm/rethnet.ts | 4 + .../hardhat-network/provider/vm/vm-adapter.ts | 3 + 24 files changed, 2228 insertions(+), 562 deletions(-) create mode 100644 crates/rethnet_evm/src/state/account.rs create mode 100644 crates/rethnet_evm/src/state/contract.rs create mode 100644 crates/rethnet_evm/src/state/history.rs create mode 100644 crates/rethnet_evm/src/state/hybrid.rs create mode 100644 crates/rethnet_evm/src/state/layered.rs create mode 100644 crates/rethnet_evm/src/state/layered/changes.rs delete mode 100644 crates/rethnet_evm/src/state/layered_state.rs create mode 100644 crates/rethnet_evm/src/state/trie.rs create mode 100644 crates/rethnet_evm/src/state/trie/account.rs diff --git a/crates/rethnet_eth/Cargo.toml b/crates/rethnet_eth/Cargo.toml index 559440fe93..cde44e5161 100644 --- a/crates/rethnet_eth/Cargo.toml +++ b/crates/rethnet_eth/Cargo.toml @@ -10,7 +10,6 @@ hash-db = { version = "0.15.2", default-features = false } hash256-std-hasher = { version = "0.15.2", default-features = false } hashbrown = { version = "0.13", default-features = false, features = ["ahash"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -hex-literal = { version = "0.3", default-features = false } open-fastrlp = { version = "0.1.2", default-features = false, features = ["derive"], optional = true } primitive-types = { version = "0.11.1", default-features = false, features = ["rlp"] } reqwest = { version = "0.11", features = ["blocking", "json"] } diff --git a/crates/rethnet_eth/src/account.rs b/crates/rethnet_eth/src/account.rs index a24252e941..4c79ca73df 100644 --- a/crates/rethnet_eth/src/account.rs +++ b/crates/rethnet_eth/src/account.rs @@ -5,14 +5,10 @@ //! Ethereum account types -use hex_literal::hex; - use crate::{trie::KECCAK_NULL_RLP, B256, U256}; -/// The KECCAK for empty code. -pub const KECCAK_EMPTY: revm_primitives::B256 = revm_primitives::B256(hex!( - "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" -)); +use revm_primitives::AccountInfo; +pub use revm_primitives::KECCAK_EMPTY; /// Basic account type. #[derive(Debug, Clone, PartialEq, Eq)] @@ -22,7 +18,7 @@ pub const KECCAK_EMPTY: revm_primitives::B256 = revm_primitives::B256(hex!( )] pub struct BasicAccount { /// Nonce of the account. - pub nonce: U256, + pub nonce: u64, /// Balance of the account. pub balance: U256, /// Storage root of the account. @@ -35,13 +31,35 @@ impl Default for BasicAccount { fn default() -> Self { BasicAccount { balance: U256::ZERO, - nonce: U256::ZERO, + nonce: 0, code_hash: KECCAK_EMPTY, storage_root: KECCAK_NULL_RLP, } } } +impl From for AccountInfo { + fn from(account: BasicAccount) -> Self { + Self { + balance: account.balance, + nonce: account.nonce, + code_hash: account.code_hash, + code: None, + } + } +} + +impl From<(&AccountInfo, B256)> for BasicAccount { + fn from((account_info, storage_root): (&AccountInfo, B256)) -> Self { + Self { + nonce: account_info.nonce, + balance: account_info.balance, + storage_root, + code_hash: account_info.code_hash, + } + } +} + impl rlp::Encodable for BasicAccount { fn rlp_append(&self, stream: &mut rlp::RlpStream) { stream.begin_list(4); diff --git a/crates/rethnet_eth/src/state.rs b/crates/rethnet_eth/src/state.rs index 9f264bdaf0..f0cd101807 100644 --- a/crates/rethnet_eth/src/state.rs +++ b/crates/rethnet_eth/src/state.rs @@ -9,16 +9,22 @@ pub type State = HashMap; pub type Storage = HashMap; /// Calculates the state root hash of the provided state. -pub fn state_root(state: &State) -> B256 { - sec_trie_root(state.iter().map(|(address, account)| { +pub fn state_root<'a, I>(state: I) -> B256 +where + I: IntoIterator, +{ + sec_trie_root(state.into_iter().map(|(address, account)| { let account = rlp::encode(account); (address, account) })) } /// Calculates the storage root hash of the provided storage. -pub fn storage_root(storage: &Storage) -> B256 { - sec_trie_root(storage.iter().map(|(index, value)| { +pub fn storage_root<'a, I>(storage: I) -> B256 +where + I: IntoIterator, +{ + sec_trie_root(storage.into_iter().map(|(index, value)| { let value = rlp::encode(value); (index.to_be_bytes::<32>(), value) })) diff --git a/crates/rethnet_evm/Cargo.toml b/crates/rethnet_evm/Cargo.toml index 04793616f0..0026673ffd 100644 --- a/crates/rethnet_evm/Cargo.toml +++ b/crates/rethnet_evm/Cargo.toml @@ -5,12 +5,17 @@ edition = "2021" [dependencies] auto_impl = { version = "1.0.1", default-features = false } +cita_trie = { git = "https://github.com/Wodann/cita-trie", rev = "60efef5", version = "4.0.0", default-features = false } hashbrown = { version = "0.13", default-features = false, features = ["ahash", "serde"] } +hasher = { git = "https://github.com/Wodann/hasher", rev = "89d3fc9", version = "0.1.4", default-features = false, features = ["hash-keccak"] } log = { version = "0.4.17", default-features = false } parking_lot = { version = "0.12.1", default-features = false } rethnet_eth = { version = "0.1.0-dev", path = "../rethnet_eth", features = ["serde"] } revm = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } # revm = { path = "../../../revm/crates/revm", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } +rlp = { version = "0.5.2", default-features = false } +serde = { version = "1.0.158", default-features = false, features = ["std"] } +serde_json = { version = "1.0.94", default-features = false, features = ["std"] } thiserror = { version = "1.0.38", default-features = false } tokio = { version = "1.21.2", default-features = false, features = ["rt-multi-thread", "sync"] } tracing = { version = "0.1.37", features = ["attributes", "std"], optional = true } diff --git a/crates/rethnet_evm/src/state.rs b/crates/rethnet_evm/src/state.rs index 5a6c631b01..c86606cfb7 100644 --- a/crates/rethnet_evm/src/state.rs +++ b/crates/rethnet_evm/src/state.rs @@ -1,14 +1,21 @@ +mod account; +mod contract; mod debug; -mod layered_state; +mod history; +mod hybrid; +mod layered; mod remote; mod request; mod sync; +mod trie; use rethnet_eth::B256; pub use self::{ debug::{AccountModifierFn, StateDebug}, - layered_state::{LayeredState, RethnetLayer}, + history::StateHistory, + hybrid::HybridState, + layered::{LayeredState, RethnetLayer}, remote::RemoteDatabase, sync::{AsyncState, SyncState}, }; diff --git a/crates/rethnet_evm/src/state/account.rs b/crates/rethnet_evm/src/state/account.rs new file mode 100644 index 0000000000..c451e3d5a9 --- /dev/null +++ b/crates/rethnet_evm/src/state/account.rs @@ -0,0 +1,17 @@ +use rethnet_eth::state::Storage; +use revm::primitives::AccountInfo; + +#[derive(Clone, Debug, Default)] +pub struct RethnetAccount { + pub info: AccountInfo, + pub storage: Storage, +} + +impl From for RethnetAccount { + fn from(info: AccountInfo) -> Self { + Self { + info, + storage: Default::default(), + } + } +} diff --git a/crates/rethnet_evm/src/state/contract.rs b/crates/rethnet_evm/src/state/contract.rs new file mode 100644 index 0000000000..a136648a72 --- /dev/null +++ b/crates/rethnet_evm/src/state/contract.rs @@ -0,0 +1,118 @@ +use hashbrown::HashMap; +use rethnet_eth::{account::KECCAK_EMPTY, B256}; +use revm::primitives::Bytecode; + +use super::{layered::LayeredChanges, RethnetLayer}; + +#[derive(Clone, Debug)] +struct ContractEntry { + code: Bytecode, + occurences: usize, +} + +impl ContractEntry { + pub fn new(code: Bytecode) -> Self { + Self { + code, + occurences: 1, + } + } + + /// Increments the number of occurences. + pub fn increment(&mut self) { + self.occurences += 1; + } + + /// Decrements the number of occurences. If no occurences are left, the [`ContractEntry`] + /// is consumed. + pub fn decrement(mut self) -> Option { + self.occurences -= 1; + + if !DELETE_UNUSED_CODE || self.occurences > 0 { + Some(self) + } else { + None + } + } +} + +#[derive(Clone, Debug)] +pub struct ContractStorage { + contracts: HashMap>, +} + +impl ContractStorage { + /// Inserts new code or, if it already exists, increments the number of occurences of + /// the code. + pub fn insert_code(&mut self, code: Bytecode) { + self.contracts + .entry(code.hash()) + .and_modify(|entry| entry.increment()) + .or_insert_with(|| ContractEntry::new(code)); + } + + /// Decremenents the number of occurences of the code corresponding to the provided code hash, + /// if it exists, and removes unused code. + pub fn remove_code(&mut self, code_hash: &B256) { + self.contracts + .entry(*code_hash) + .and_replace_entry_with(|_code, entry| entry.decrement()); + } +} + +impl ContractStorage { + /// Retrieves the contract code corresponding to the provided hash. + pub fn get(&self, code_hash: &B256) -> Option<&Bytecode> { + self.contracts.get(code_hash).map(|entry| &entry.code) + } +} + +impl ContractStorage { + /// Retrieves the contract code corresponding to the provided hash. + pub fn get(&self, code_hash: &B256) -> Option<&Bytecode> { + self.contracts.get(code_hash).and_then(|entry| { + if entry.occurences > 0 { + Some(&entry.code) + } else { + None + } + }) + } +} + +impl Default for ContractStorage { + fn default() -> Self { + let mut contracts = HashMap::new(); + contracts.insert(KECCAK_EMPTY, ContractEntry::new(Bytecode::new())); + + Self { contracts } + } +} + +impl From<&LayeredChanges> for ContractStorage { + fn from(changes: &LayeredChanges) -> Self { + let mut storage = Self::default(); + + changes.iter().for_each(|layer| { + layer + .contracts() + .contracts + .iter() + .for_each(|(code_hash, entry)| { + if entry.occurences > 0 { + storage.contracts.insert( + *code_hash, + ContractEntry { + code: entry.code.clone(), + occurences: entry.occurences, + }, + ); + } else { + storage.contracts.remove(code_hash); + } + }) + }); + + storage + } +} diff --git a/crates/rethnet_evm/src/state/debug.rs b/crates/rethnet_evm/src/state/debug.rs index bf1f9aec87..bc97960d49 100644 --- a/crates/rethnet_evm/src/state/debug.rs +++ b/crates/rethnet_evm/src/state/debug.rs @@ -4,14 +4,16 @@ use auto_impl::auto_impl; use rethnet_eth::{Address, B256, U256}; use revm::primitives::{AccountInfo, Bytecode}; +type BoxedAccountModifierFn = Box) + Send>; + /// Debuggable function type for modifying account information. pub struct AccountModifierFn { - inner: Box) + Send>, + inner: BoxedAccountModifierFn, } impl AccountModifierFn { /// Constructs an [`AccountModifierDebuggableFn`] from the provided function. - pub fn new(func: Box) + Send>) -> Self { + pub fn new(func: BoxedAccountModifierFn) -> Self { Self { inner: func } } } @@ -61,6 +63,9 @@ pub trait StateDebug { /// Removes and returns the account at the specified address, if it exists. fn remove_account(&mut self, address: Address) -> Result, Self::Error>; + /// Serializes the state using ordering of addresses and storage indices. + fn serialize(&mut self) -> String; + /// Sets the storage slot at the specified address and index to the provided value. fn set_account_storage_slot( &mut self, @@ -69,22 +74,6 @@ pub trait StateDebug { value: U256, ) -> Result<(), Self::Error>; - /// Reverts the state to match the specified state root. - fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error>; - /// Retrieves the storage root of the database. fn state_root(&mut self) -> Result; - - /// Creates a checkpoint that can be reverted to using [`revert`]. - fn checkpoint(&mut self) -> Result<(), Self::Error>; - - /// Reverts to the previous checkpoint, created using [`checkpoint`]. - fn revert(&mut self) -> Result<(), Self::Error>; - - /// Makes a snapshot of the database that's retained until [`remove_snapshot`] is called. Returns the snapshot's identifier and whether - /// that snapshot already existed. - fn make_snapshot(&mut self) -> (B256, bool); - - /// Removes the snapshot corresponding to the specified state root, if it exists. Returns whether a snapshot was removed. - fn remove_snapshot(&mut self, state_root: &B256) -> bool; } diff --git a/crates/rethnet_evm/src/state/history.rs b/crates/rethnet_evm/src/state/history.rs new file mode 100644 index 0000000000..55590aa1b0 --- /dev/null +++ b/crates/rethnet_evm/src/state/history.rs @@ -0,0 +1,25 @@ +use auto_impl::auto_impl; +use rethnet_eth::B256; + +/// A trait for debug operation on a database. +#[auto_impl(Box)] +pub trait StateHistory { + /// The database's error type. + type Error; + + /// Reverts the state to match the specified state root. + fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error>; + + /// Creates a checkpoint that can be reverted to using [`revert`]. + fn checkpoint(&mut self) -> Result<(), Self::Error>; + + /// Reverts to the previous checkpoint, created using [`checkpoint`]. + fn revert(&mut self) -> Result<(), Self::Error>; + + /// Makes a snapshot of the database that's retained until [`remove_snapshot`] is called. Returns the snapshot's identifier and whether + /// that snapshot already existed. + fn make_snapshot(&mut self) -> (B256, bool); + + /// Removes the snapshot corresponding to the specified state root, if it exists. Returns whether a snapshot was removed. + fn remove_snapshot(&mut self, state_root: &B256) -> bool; +} diff --git a/crates/rethnet_evm/src/state/hybrid.rs b/crates/rethnet_evm/src/state/hybrid.rs new file mode 100644 index 0000000000..53530210d3 --- /dev/null +++ b/crates/rethnet_evm/src/state/hybrid.rs @@ -0,0 +1,277 @@ +use std::fmt::Debug; + +use hashbrown::HashMap; +use rethnet_eth::{Address, B256, U256}; +use revm::{ + db::StateRef, + primitives::{Account, AccountInfo, Bytecode, KECCAK_EMPTY}, + DatabaseCommit, +}; + +use super::{ + history::StateHistory, + layered::LayeredChanges, + trie::{AccountTrie, TrieState}, + AccountModifierFn, RethnetLayer, StateDebug, StateError, +}; + +#[derive(Debug)] +struct Snapshot { + pub changes: LayeredChanges, + pub trie: TrieState, +} + +/// A state consisting of layers. +#[derive(Debug, Default)] +pub struct HybridState { + trie: TrieState, + changes: LayeredChanges, + snapshots: HashMap>, +} + +impl>> HybridState { + /// Creates a [`HybridState`] with the provided layer at the bottom. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn with_accounts(accounts: HashMap) -> Self { + let latest_state = TrieState::with_accounts(AccountTrie::with_accounts(&accounts)); + let layer = accounts.into(); + + Self { + trie: latest_state, + changes: LayeredChanges::with_layer(layer), + snapshots: HashMap::new(), + } + } +} + +impl StateRef for HybridState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn basic(&self, address: Address) -> Result, Self::Error> { + self.trie.basic(address) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn code_by_hash(&self, code_hash: B256) -> Result { + self.trie.code_by_hash(code_hash) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn storage(&self, address: Address, index: U256) -> Result { + self.trie.storage(address, index) + } +} + +impl DatabaseCommit for HybridState { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn commit(&mut self, changes: HashMap) { + self.changes.apply(&changes); + self.trie.commit(changes); + } +} + +impl StateDebug for HybridState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { + self.trie.account_storage_root(address) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn insert_account( + &mut self, + address: Address, + account_info: AccountInfo, + ) -> Result<(), Self::Error> { + self.trie.insert_account(address, account_info.clone())?; + self.changes.account_or_insert_mut(&address).info = account_info; + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn modify_account( + &mut self, + address: Address, + modifier: AccountModifierFn, + ) -> Result<(), Self::Error> { + let mut account_info = self.trie.basic(address)?.map_or_else( + || AccountInfo { + code: None, + ..AccountInfo::default() + }, + |mut account_info| { + // Fill the bytecode + if account_info.code_hash != KECCAK_EMPTY { + account_info.code = Some( + self.trie + .code_by_hash(account_info.code_hash) + .expect("Code must exist"), + ); + } + + account_info + }, + ); + + let old_code_hash = account_info.code_hash; + + modifier( + &mut account_info.balance, + &mut account_info.nonce, + &mut account_info.code, + ); + + let new_code = account_info.code.take(); + let new_code_hash = new_code.as_ref().map_or(KECCAK_EMPTY, |code| code.hash()); + account_info.code_hash = new_code_hash; + + let code_changed = old_code_hash != new_code_hash; + if code_changed { + if let Some(new_code) = new_code { + self.trie.insert_code(new_code.clone()); + self.changes.insert_code(new_code); + } + + self.trie.remove_code(&old_code_hash); + self.changes.remove_code(&old_code_hash); + } + + self.trie.insert_account(address, account_info.clone())?; + self.changes.account_or_insert_mut(&address).info = account_info; + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn remove_account(&mut self, address: Address) -> Result, Self::Error> { + Ok(if self.trie.remove_account(address).unwrap().is_some() { + self.changes.remove_account(&address) + } else { + None + }) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn serialize(&mut self) -> String { + self.trie.serialize() + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_account_storage_slot( + &mut self, + address: Address, + index: U256, + value: U256, + ) -> Result<(), Self::Error> { + self.trie.set_account_storage_slot(address, index, value)?; + + self.changes + .account_or_insert_mut(&address) + .storage + .insert(index, value); + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn state_root(&mut self) -> Result { + self.trie.state_root() + } +} + +impl StateHistory for HybridState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn make_snapshot(&mut self) -> (B256, bool) { + let state_root = self.state_root().unwrap(); + + let mut exists = true; + self.snapshots.entry(state_root).or_insert_with(|| { + exists = false; + + let mut changes = self.changes.clone(); + changes.last_layer_mut().set_state_root(state_root); + + Snapshot { + changes, + trie: self.trie.clone(), + } + }); + + (state_root, exists) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn remove_snapshot(&mut self, state_root: &B256) -> bool { + self.snapshots.remove(state_root).is_some() + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { + // Ensure the last layer has a state root + if !self.changes.last_layer_mut().has_state_root() { + let state_root = self.state_root()?; + self.changes.last_layer_mut().set_state_root(state_root); + } + + if let Some(Snapshot { + changes, + trie: latest_state, + }) = self.snapshots.remove(state_root) + { + self.trie = latest_state; + self.changes = changes; + + return Ok(()); + } + + let inverted_layer_id = self + .changes + .iter() + .enumerate() + .find_map(|(layer_id, layer)| { + if *layer.state_root().unwrap() == *state_root { + Some(layer_id) + } else { + None + } + }); + + if let Some(layer_id) = inverted_layer_id { + let layer_id = self.changes.last_layer_id() - layer_id; + + self.changes.revert_to_layer(layer_id); + self.trie = TrieState::from(&self.changes); + + Ok(()) + } else { + Err(StateError::InvalidStateRoot(*state_root)) + } + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn checkpoint(&mut self) -> Result<(), Self::Error> { + let state_root = self.state_root()?; + self.changes.last_layer_mut().set_state_root(state_root); + + self.changes.add_layer_default(); + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn revert(&mut self) -> Result<(), Self::Error> { + let last_layer_id = self.changes.last_layer_id(); + if last_layer_id > 0 { + self.changes.revert_to_layer(last_layer_id - 1); + self.trie = TrieState::from(&self.changes); + Ok(()) + } else { + Err(StateError::CannotRevert) + } + } +} diff --git a/crates/rethnet_evm/src/state/layered.rs b/crates/rethnet_evm/src/state/layered.rs new file mode 100644 index 0000000000..b84e966ba4 --- /dev/null +++ b/crates/rethnet_evm/src/state/layered.rs @@ -0,0 +1,277 @@ +mod changes; + +pub use changes::{LayeredChanges, RethnetLayer}; + +use std::fmt::Debug; + +use hashbrown::HashMap; +use rethnet_eth::{ + account::BasicAccount, + state::{state_root, storage_root}, + Address, B256, U256, +}; +use revm::{ + db::StateRef, + primitives::{Account, AccountInfo, Bytecode, KECCAK_EMPTY}, + DatabaseCommit, +}; + +use super::{history::StateHistory, AccountModifierFn, StateDebug, StateError}; + +/// A state consisting of layers. +#[derive(Debug, Default)] +pub struct LayeredState { + changes: LayeredChanges, + /// Snapshots + snapshots: HashMap>, +} + +impl>> LayeredState { + /// Creates a [`LayeredState`] with the provided layer at the bottom. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn with_accounts(accounts: HashMap) -> Self { + let layer = accounts.into(); + + Self { + changes: LayeredChanges::with_layer(layer), + snapshots: HashMap::new(), + } + } +} + +impl StateRef for LayeredState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn basic(&self, address: Address) -> Result, Self::Error> { + Ok(self + .changes + .account(&address) + .map(|account| account.info.clone())) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn code_by_hash(&self, code_hash: B256) -> Result { + self.changes + .code_by_hash(&code_hash) + .map(Clone::clone) + .ok_or(StateError::InvalidCodeHash(code_hash)) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn storage(&self, address: Address, index: U256) -> Result { + Ok(self + .changes + .account(&address) + .and_then(|account| account.storage.get(&index)) + .cloned() + .unwrap_or(U256::ZERO)) + } +} + +impl DatabaseCommit for LayeredState { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn commit(&mut self, changes: HashMap) { + self.changes.apply(&changes); + } +} + +impl StateDebug for LayeredState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { + Ok(self + .changes + .account(address) + .map(|account| storage_root(&account.storage))) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn insert_account( + &mut self, + address: Address, + account_info: AccountInfo, + ) -> Result<(), Self::Error> { + self.changes.account_or_insert_mut(&address).info = account_info; + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn modify_account( + &mut self, + address: Address, + modifier: AccountModifierFn, + ) -> Result<(), Self::Error> { + let mut account_info = self.changes.account_or_insert_mut(&address).info.clone(); + + // Fill the bytecode + if account_info.code_hash != KECCAK_EMPTY { + account_info.code = Some( + self.changes + .code_by_hash(&account_info.code_hash) + .cloned() + .expect("Code must exist"), + ); + } + + let old_code_hash = account_info.code_hash; + + modifier( + &mut account_info.balance, + &mut account_info.nonce, + &mut account_info.code, + ); + + let new_code = account_info.code.take(); + let new_code_hash = new_code.as_ref().map_or(KECCAK_EMPTY, |code| code.hash()); + account_info.code_hash = new_code_hash; + + let code_change = old_code_hash != new_code_hash; + if code_change { + if let Some(new_code) = new_code { + self.changes.insert_code(new_code); + } + + self.changes.remove_code(&old_code_hash); + } + + self.changes.account_or_insert_mut(&address).info = account_info; + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn remove_account(&mut self, address: Address) -> Result, Self::Error> { + Ok(self.changes.remove_account(&address)) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn serialize(&mut self) -> String { + self.changes.serialize() + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_account_storage_slot( + &mut self, + address: Address, + index: U256, + value: U256, + ) -> Result<(), Self::Error> { + self.changes + .account_or_insert_mut(&address) + .storage + .insert(index, value); + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn state_root(&mut self) -> Result { + let mut state = HashMap::new(); + + self.changes + .iter() + .flat_map(|layer| layer.accounts()) + .for_each(|(address, account)| { + state + .entry(*address) + .or_insert(account.as_ref().map(|account| BasicAccount { + nonce: account.info.nonce, + balance: account.info.balance, + storage_root: storage_root(&account.storage), + code_hash: account.info.code_hash, + })); + }); + + let state = state + .iter() + .filter_map(|(address, account)| account.as_ref().map(|account| (address, account))); + + Ok(state_root(state)) + } +} + +impl StateHistory for LayeredState { + type Error = StateError; + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn make_snapshot(&mut self) -> (B256, bool) { + let state_root = self.state_root().unwrap(); + + let mut exists = true; + self.snapshots.entry(state_root).or_insert_with(|| { + exists = false; + + let mut snapshot = self.changes.clone(); + snapshot.last_layer_mut().set_state_root(state_root); + + snapshot + }); + + (state_root, exists) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn remove_snapshot(&mut self, state_root: &B256) -> bool { + self.snapshots.remove(state_root).is_some() + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { + // Ensure the last layer has a state root + if !self.changes.last_layer_mut().has_state_root() { + let state_root = self.state_root()?; + self.changes.last_layer_mut().set_state_root(state_root); + } + + if let Some(snapshot) = self.snapshots.remove(state_root) { + self.changes = snapshot; + + return Ok(()); + } + + let inverted_layer_id = self + .changes + .iter() + .enumerate() + .find_map(|(layer_id, layer)| { + if *layer.state_root().unwrap() == *state_root { + Some(layer_id) + } else { + None + } + }); + + if let Some(inverted_layer_id) = inverted_layer_id { + let layer_id = self.changes.last_layer_id() - inverted_layer_id; + self.changes.revert_to_layer(layer_id); + + Ok(()) + } else { + Err(StateError::InvalidStateRoot(*state_root)) + } + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn checkpoint(&mut self) -> Result<(), Self::Error> { + let state_root = self.state_root()?; + self.changes.last_layer_mut().set_state_root(state_root); + + self.changes.add_layer_default(); + + Ok(()) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn revert(&mut self) -> Result<(), Self::Error> { + let last_layer_id = self.changes.last_layer_id(); + if last_layer_id > 0 { + self.changes.revert_to_layer(last_layer_id - 1); + Ok(()) + } else { + Err(StateError::CannotRevert) + } + } +} diff --git a/crates/rethnet_evm/src/state/layered/changes.rs b/crates/rethnet_evm/src/state/layered/changes.rs new file mode 100644 index 0000000000..8d02e80b17 --- /dev/null +++ b/crates/rethnet_evm/src/state/layered/changes.rs @@ -0,0 +1,329 @@ +use std::{collections::BTreeMap, fmt::Debug}; + +use cita_trie::Hasher; +use hashbrown::HashMap; +use hasher::HasherKeccak; +use rethnet_eth::{account::KECCAK_EMPTY, state::storage_root, Address, B256, U256}; +use revm::primitives::{Account, AccountInfo, Bytecode}; + +use crate::state::{account::RethnetAccount, contract::ContractStorage}; + +#[derive(Clone, Debug)] +pub struct LayeredChanges { + stack: Vec, +} + +impl LayeredChanges { + /// Creates [`LayeredChanges`] with the provided layer at the bottom. + pub fn with_layer(layer: Layer) -> Self { + Self { stack: vec![layer] } + } + + /// Returns the index of the top layer. + pub fn last_layer_id(&self) -> usize { + self.stack.len() - 1 + } + + /// Returns a mutable reference to the top layer. + pub fn last_layer_mut(&mut self) -> &mut Layer { + // The `LayeredState` always has at least one layer + self.stack.last_mut().unwrap() + } + + /// Returns an iterator over the object's layers. + pub fn iter(&self) -> impl Iterator { + self.stack.iter().rev() + } + + /// Returns a reverse iterator over the object's layers, oldest to newest. + pub fn rev(&self) -> impl Iterator { + self.stack.iter() + } +} + +impl LayeredChanges { + /// Adds the provided layer to the top, returning its index and a + /// mutable reference to the layer. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn add_layer(&mut self, layer: Layer) -> (usize, &mut Layer) { + let layer_id = self.stack.len(); + self.stack.push(layer); + (layer_id, self.stack.last_mut().unwrap()) + } + + /// Reverts to the layer with specified `layer_id`, removing all + /// layers above it. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn revert_to_layer(&mut self, layer_id: usize) { + assert!(layer_id < self.stack.len(), "Invalid layer id."); + self.stack.truncate(layer_id + 1); + } +} + +impl LayeredChanges { + /// Adds a default layer to the top, returning its index and a + /// mutable reference to the layer. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn add_layer_default(&mut self) -> (usize, &mut Layer) { + self.add_layer(Layer::default()) + } +} + +impl Default for LayeredChanges { + fn default() -> Self { + Self { + stack: vec![Layer::default()], + } + } +} + +/// A layer with information needed for [`Rethnet`]. +#[derive(Clone, Debug, Default)] +pub struct RethnetLayer { + /// Accounts, where the Option signals deletion. + accounts: HashMap>, + /// Code hash -> Address + contracts: ContractStorage, + /// Cached state root + state_root: Option, +} + +impl RethnetLayer { + /// Retrieves an iterator over all accounts. + pub fn accounts(&self) -> impl Iterator)> { + self.accounts.iter() + } + + /// Retrieves the contract storage + pub fn contracts(&self) -> &ContractStorage { + &self.contracts + } + + /// Returns whether the layer has a state root. + pub fn has_state_root(&self) -> bool { + self.state_root.is_some() + } + + /// Retrieves the layer's state root. + pub fn state_root(&self) -> Option<&B256> { + self.state_root.as_ref() + } + + /// Sets the layer's state root. + pub fn set_state_root(&mut self, state_root: B256) { + self.state_root = Some(state_root); + } +} + +impl From> for RethnetLayer { + fn from(accounts: HashMap) -> Self { + let mut accounts: HashMap> = accounts + .into_iter() + .map(|(address, account_info)| (address, Some(account_info.into()))) + .collect(); + + let mut contracts = ContractStorage::default(); + + accounts + .values_mut() + .filter_map(|account| { + account + .as_mut() + .and_then(|account| account.info.code.take()) + }) + .for_each(|code| { + if code.hash() != KECCAK_EMPTY { + contracts.insert_code(code); + } + }); + + Self { + accounts, + contracts, + state_root: None, + } + } +} + +impl LayeredChanges { + /// Retrieves a reference to the account corresponding to the address, if it exists. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn account(&self, address: &Address) -> Option<&RethnetAccount> { + self.iter() + .find_map(|layer| layer.accounts.get(address).map(Option::as_ref)) + .flatten() + } + + /// Retrieves a mutable reference to the account corresponding to the address, if it exists. + /// Otherwise, inserts a new account. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn account_or_insert_mut(&mut self, address: &Address) -> &mut RethnetAccount { + // WORKAROUND: https://blog.rust-lang.org/2022/08/05/nll-by-default.html + if self.last_layer_mut().accounts.contains_key(address) { + let was_deleted = self + .last_layer_mut() + .accounts + .get(address) + .unwrap() + .is_none(); + + if !was_deleted { + return self + .last_layer_mut() + .accounts + .get_mut(address) + .unwrap() + .as_mut() + .unwrap(); + } + } + + let account = self.account(address).cloned().unwrap_or_default(); + + self.last_layer_mut() + .accounts + .insert_unique_unchecked(*address, Some(account)) + .1 + .as_mut() + .unwrap() + } + + /// Applies the provided changes to the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn apply(&mut self, changes: &HashMap) { + changes.iter().for_each(|(address, account)| { + if account.is_destroyed || account.is_empty() { + // Removes account only if it exists, so safe to use for empty, touched accounts + self.remove_account(address); + } else { + let old_account = self.account_or_insert_mut(address); + + if account.storage_cleared { + old_account.storage.clear(); + } + + account.storage.iter().for_each(|(index, value)| { + let value = value.present_value(); + if value == U256::ZERO { + old_account.storage.remove(index); + } else { + old_account.storage.insert(*index, value); + } + }); + + let mut account_info = account.info.clone(); + + let old_code_hash = old_account.info.code_hash; + let code_changed = old_code_hash != account_info.code_hash; + + let new_code = account_info.code.take(); + old_account.info = account_info; + + if code_changed { + if let Some(new_code) = new_code { + self.insert_code(new_code); + } + + self.remove_code(&old_code_hash); + } + } + }); + } + + /// Retrieves the code corresponding to the specified code hash. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn code_by_hash(&self, code_hash: &B256) -> Option<&Bytecode> { + self.iter().find_map(|layer| layer.contracts.get(code_hash)) + } + + /// Removes the [`AccountInfo`] corresponding to the specified address. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn remove_account(&mut self, address: &Address) -> Option { + if let Some(account) = self.account(address) { + let account_info = account.info.clone(); + + if account.info.code_hash != KECCAK_EMPTY { + debug_assert!(account.info.code.is_none()); + + let code_hash = account.info.code_hash; + + self.last_layer_mut().contracts.remove_code(&code_hash); + } + + // Insert `None` to signal that the account was deleted + self.last_layer_mut().accounts.insert(*address, None); + + return Some(account_info); + } + + None + } + + /// Serializes the state using ordering of addresses and storage indices. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn serialize(&self) -> String { + let mut state = BTreeMap::new(); + + #[derive(serde::Serialize)] + struct StateAccount { + /// Balance of the account. + pub balance: U256, + /// Code hash of the account. + pub code_hash: B256, + /// Nonce of the account. + pub nonce: u64, + /// Storage + pub storage: BTreeMap, + /// Storage root of the account. + pub storage_root: B256, + } + + self.iter() + .flat_map(|layer| layer.accounts()) + .for_each(|(address, account)| { + state.entry(*address).or_insert_with(|| { + account.as_ref().map(|account| { + let storage_root = storage_root(&account.storage); + + // Sort entries + let storage: BTreeMap = account + .storage + .iter() + .map(|(index, value)| { + let hashed_index = + HasherKeccak::new().digest(&index.to_be_bytes::<32>()); + + (B256::from_slice(&hashed_index), *value) + }) + .collect(); + + StateAccount { + balance: account.info.balance, + nonce: account.info.nonce, + code_hash: account.info.code_hash, + storage_root, + storage, + } + }) + }); + }); + + // Remove deleted entries + let state: BTreeMap<_, _> = state + .into_iter() + .filter_map(|(address, account)| account.map(|account| (address, account))) + .collect(); + + serde_json::to_string_pretty(&state).unwrap() + } + + /// Inserts the provided bytecode using its hash, potentially overwriting an existing value. + pub fn insert_code(&mut self, code: Bytecode) { + self.last_layer_mut().contracts.insert_code(code); + } + + /// Removes the code corresponding to the provided hash, if it exists. + pub fn remove_code(&mut self, code_hash: &B256) { + self.last_layer_mut().contracts.remove_code(code_hash); + } +} diff --git a/crates/rethnet_evm/src/state/layered_state.rs b/crates/rethnet_evm/src/state/layered_state.rs deleted file mode 100644 index b6a0f8564a..0000000000 --- a/crates/rethnet_evm/src/state/layered_state.rs +++ /dev/null @@ -1,515 +0,0 @@ -use hashbrown::HashMap; -use rethnet_eth::{ - account::BasicAccount, - state::{state_root, storage_root}, - trie::KECCAK_NULL_RLP, - Address, B256, U256, -}; -use revm::{ - db::State, - primitives::{Account, AccountInfo, Bytecode, KECCAK_EMPTY}, - DatabaseCommit, -}; - -use super::{AccountModifierFn, StateDebug, StateError}; - -/// A state consisting of layers. -#[derive(Clone, Debug)] -pub struct LayeredState { - stack: Vec, - /// Snapshots - snapshots: HashMap>, // naive implementation -} - -impl LayeredState { - /// Creates a [`LayeredState`] with the provided layer at the bottom. - pub fn with_layer(layer: Layer) -> Self { - Self { - stack: vec![layer], - snapshots: HashMap::new(), - } - } - - /// Returns the index of the top layer. - pub fn last_layer_id(&self) -> usize { - self.stack.len() - 1 - } - - /// Returns a mutable reference to the top layer. - pub fn last_layer_mut(&mut self) -> &mut Layer { - // The `LayeredState` always has at least one layer - self.stack.last_mut().unwrap() - } - - /// Adds the provided layer to the top, returning its index and a - /// mutable reference to the layer. - pub fn add_layer(&mut self, layer: Layer) -> (usize, &mut Layer) { - let layer_id = self.stack.len(); - self.stack.push(layer); - (layer_id, self.stack.last_mut().unwrap()) - } - - /// Reverts to the layer with specified `layer_id`, removing all - /// layers above it. - pub fn revert_to_layer(&mut self, layer_id: usize) { - assert!(layer_id < self.stack.len(), "Invalid layer id."); - self.stack.truncate(layer_id + 1); - } - - /// Returns an iterator over the object's layers. - pub fn iter(&self) -> impl Iterator { - self.stack.iter().rev() - } -} - -impl LayeredState { - /// Adds a default layer to the top, returning its index and a - /// mutable reference to the layer. - pub fn add_layer_default(&mut self) -> (usize, &mut Layer) { - self.add_layer(Layer::default()) - } -} - -impl Default for LayeredState { - fn default() -> Self { - Self { - stack: vec![Layer::default()], - snapshots: HashMap::new(), - } - } -} - -/// A layer with information needed for [`Rethnet`]. -#[derive(Clone, Debug, Default)] -pub struct RethnetLayer { - /// Address -> AccountInfo - account_infos: HashMap>, - /// Address -> Storage - storage: HashMap>>, - /// Code hash -> Address - contracts: HashMap, - /// Cached state root - state_root: Option, -} - -impl RethnetLayer { - /// Creates a `RethnetLayer` with the provided genesis accounts. - pub fn with_genesis_accounts(genesis_accounts: HashMap) -> Self { - let genesis_accounts = genesis_accounts - .into_iter() - .map(|(address, account_info)| (address, Some(account_info))) - .collect(); - - Self { - account_infos: genesis_accounts, - ..Default::default() - } - } - - /// Returns whether the layer has a state root. - pub fn has_state_root(&self) -> bool { - self.state_root.is_some() - } - - /// Insert the provided `AccountInfo` at the specified `address`. - pub fn insert_account(&mut self, address: Address, mut account_info: AccountInfo) { - if let Some(code) = account_info.code.take() { - if !code.is_empty() { - account_info.code_hash = code.hash(); - self.contracts.insert(code.hash(), code); - } - } - - if account_info.code_hash.is_zero() { - account_info.code_hash = KECCAK_EMPTY; - } - - self.account_infos.insert(address, Some(account_info)); - } -} - -impl LayeredState { - /// Retrieves a reference to the account corresponding to the address, if it exists. - pub fn account(&self, address: &Address) -> Option<&AccountInfo> { - self.iter() - .find_map(|layer| { - layer - .account_infos - .get(address) - .map(|account_info| account_info.as_ref()) - }) - .flatten() - } - - /// Retrieves a mutable reference to the account corresponding to the address, if it exists. - pub fn account_mut(&mut self, address: &Address) -> Option<&mut AccountInfo> { - // WORKAROUND: https://blog.rust-lang.org/2022/08/05/nll-by-default.html - if self.last_layer_mut().account_infos.contains_key(address) { - return self - .last_layer_mut() - .account_infos - .get_mut(address) - .and_then(|account_info| account_info.as_mut()); - } - - self.account(address).cloned().map(|account_info| { - self.last_layer_mut() - .account_infos - .insert_unique_unchecked(*address, Some(account_info)) - .1 - .as_mut() - .unwrap() - }) - } - - /// Retrieves a mutable reference to the account corresponding to the address, if it exists. - /// Otherwise, inserts a new account. - pub fn account_or_insert_mut(&mut self, address: &Address) -> &mut AccountInfo { - // WORKAROUND: https://blog.rust-lang.org/2022/08/05/nll-by-default.html - if self.last_layer_mut().account_infos.contains_key(address) { - let was_deleted = self - .last_layer_mut() - .account_infos - .get(address) - .unwrap() - .is_none(); - - if !was_deleted { - return self - .last_layer_mut() - .account_infos - .get_mut(address) - .unwrap() - .as_mut() - .unwrap(); - } - } - - let account_info = self.account(address).cloned().unwrap_or(AccountInfo { - balance: U256::ZERO, - nonce: 0, - code_hash: KECCAK_EMPTY, - code: None, - }); - - self.last_layer_mut() - .account_infos - .insert_unique_unchecked(*address, Some(account_info)) - .1 - .as_mut() - .unwrap() - } - - /// Removes the [`AccountInfo`] corresponding to the specified address. - fn remove_account(&mut self, address: &Address) -> Option { - let account_info = self - .iter() - .find_map(|layer| layer.account_infos.get(address)) - .cloned() - .flatten(); - - if let Some(account_info) = &account_info { - debug_assert!(account_info.code.is_none()); - - let code_hash = account_info.code_hash; - - self.last_layer_mut() - .contracts - .insert(code_hash, Bytecode::new()); - - // Write None to signal that the account was deleted - self.last_layer_mut().account_infos.insert(*address, None); - } - - let storage = self.iter().find_map(|layer| layer.storage.get(address)); - - if let Some(Some(_)) = storage { - // Write None to signal that the account's storage was deleted - self.last_layer_mut().storage.insert(*address, None); - } - - account_info - } -} - -impl State for LayeredState { - type Error = StateError; - - fn basic(&mut self, address: Address) -> Result, Self::Error> { - let account = self - .iter() - .find_map(|layer| layer.account_infos.get(&address)) - .cloned() - .flatten(); - - Ok(account) - } - - fn code_by_hash(&mut self, code_hash: B256) -> Result { - if code_hash == KECCAK_EMPTY { - return Ok(Bytecode::new()); - } - - self.iter() - .find_map(|layer| layer.contracts.get(&code_hash).cloned()) - .ok_or(StateError::InvalidCodeHash(code_hash)) - } - - fn storage(&mut self, address: Address, index: U256) -> Result { - Ok(self - .iter() - .find_map(|layer| layer.storage.get(&address).map(|storage| storage.as_ref())) - .flatten() - .and_then(|storage| storage.get(&index)) - .cloned() - .unwrap_or(U256::ZERO)) - } -} - -impl DatabaseCommit for LayeredState { - fn commit(&mut self, changes: HashMap) { - changes.into_iter().for_each(|(address, account)| { - if account.is_empty() || account.is_destroyed { - self.remove_account(&address); - } else { - self.last_layer_mut().insert_account(address, account.info); - - let storage = if self.last_layer_mut().storage.contains_key(&address) { - let storage = self.last_layer_mut().storage.get_mut(&address).unwrap(); - - let was_deleted = storage.is_none(); - if was_deleted { - storage.replace(HashMap::new()); - } - - storage.as_mut().unwrap() - } else { - let storage = self - .iter() - .find_map(|layer| layer.storage.get(&address)) - .cloned() - .flatten() - .unwrap_or_default(); - - self.last_layer_mut() - .storage - .insert_unique_unchecked(address, Some(storage)) - .1 - .as_mut() - .unwrap() - }; - - if account.storage_cleared { - storage.clear(); - } - - account.storage.into_iter().for_each(|(index, value)| { - let value = value.present_value(); - if value == U256::ZERO { - storage.remove(&index); - } else { - storage.insert(index, value); - } - }); - } - }); - } -} - -impl StateDebug for LayeredState { - type Error = StateError; - - fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { - Ok(self - .iter() - .find_map(|layer| layer.storage.get(address)) - .map(|storage| storage.as_ref().map_or(KECCAK_NULL_RLP, storage_root))) - } - - fn insert_account( - &mut self, - address: Address, - account_info: AccountInfo, - ) -> Result<(), Self::Error> { - self.last_layer_mut().insert_account(address, account_info); - - Ok(()) - } - - fn make_snapshot(&mut self) -> (B256, bool) { - let state_root = self.state_root().unwrap(); - - let mut exists = true; - self.snapshots.entry(state_root).or_insert_with(|| { - exists = false; - - let mut snapshot = self.stack.clone(); - if let Some(layer) = snapshot.last_mut() { - layer.state_root.replace(state_root); - } - snapshot - }); - - (state_root, exists) - } - - fn modify_account( - &mut self, - address: Address, - modifier: AccountModifierFn, - ) -> Result<(), Self::Error> { - let account_info = self.account_or_insert_mut(&address); - let old_code_hash = account_info.code_hash; - - modifier( - &mut account_info.balance, - &mut account_info.nonce, - &mut account_info.code, - ); - - if let Some(code) = account_info.code.take() { - let new_code_hash = code.hash(); - - if old_code_hash != new_code_hash { - account_info.code_hash = new_code_hash; - - let last_layer = self.last_layer_mut(); - - // The old contract should now return empty bytecode - last_layer.contracts.insert(old_code_hash, Bytecode::new()); - - last_layer.contracts.insert(new_code_hash, code); - } - } - - Ok(()) - } - - fn remove_account(&mut self, address: Address) -> Result, Self::Error> { - Ok(self.remove_account(&address)) - } - - fn remove_snapshot(&mut self, state_root: &B256) -> bool { - self.snapshots.remove(state_root).is_some() - } - - fn set_account_storage_slot( - &mut self, - address: Address, - index: U256, - value: U256, - ) -> Result<(), Self::Error> { - self.last_layer_mut() - .storage - .entry(address) - .and_modify(|entry| { - let was_deleted = entry.is_none(); - if was_deleted { - entry.replace(HashMap::new()); - } - - entry.as_mut().unwrap().insert(index, value); - }) - .or_insert_with(|| { - let mut account_storage = HashMap::new(); - account_storage.insert(index, value); - - Some(account_storage) - }); - - Ok(()) - } - - fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { - // Ensure the last layer has a state root - if !self.last_layer_mut().has_state_root() { - let state_root = self.state_root()?; - self.last_layer_mut().state_root.replace(state_root); - } - - if let Some(snapshot) = self.snapshots.remove(state_root) { - self.stack = snapshot; - - return Ok(()); - } - - let layer_id = self.stack.iter().enumerate().find_map(|(layer_id, layer)| { - if layer.state_root.unwrap() == *state_root { - Some(layer_id) - } else { - None - } - }); - - if let Some(layer_id) = layer_id { - self.stack.truncate(layer_id + 1); - - Ok(()) - } else { - Err(StateError::InvalidStateRoot(*state_root)) - } - } - - fn state_root(&mut self) -> Result { - let mut storage = HashMap::new(); - - self.iter().flat_map(|layer| layer.storage.iter()).for_each( - |(address, account_storage)| { - storage.entry(*address).or_insert(account_storage.clone()); - }, - ); - - let storage_roots: HashMap = storage - .into_iter() - .filter_map(|(address, storage)| { - storage.map(|storage| (address, storage_root(&storage))) - }) - .collect(); - - let mut state = HashMap::new(); - - self.iter() - .flat_map(|layer| layer.account_infos.iter()) - .for_each(|(address, account_info)| { - let storage_root = storage_roots - .get(address) - .cloned() - .unwrap_or(KECCAK_NULL_RLP); - - state - .entry(*address) - .or_insert(account_info.as_ref().map(|account_info| BasicAccount { - nonce: U256::from(account_info.nonce), - balance: account_info.balance, - storage_root, - code_hash: account_info.code_hash, - })); - }); - - let state: HashMap = state - .into_iter() - .filter_map(|(address, account)| account.map(|account| (address, account))) - .collect(); - - Ok(state_root(&state)) - } - - fn checkpoint(&mut self) -> Result<(), Self::Error> { - let state_root = self.state_root()?; - self.last_layer_mut().state_root.replace(state_root); - - self.add_layer_default(); - - Ok(()) - } - - fn revert(&mut self) -> Result<(), Self::Error> { - let last_layer_id = self.last_layer_id(); - if last_layer_id > 0 { - self.revert_to_layer(last_layer_id - 1); - Ok(()) - } else { - Err(StateError::CannotRevert) - } - } -} diff --git a/crates/rethnet_evm/src/state/request.rs b/crates/rethnet_evm/src/state/request.rs index 6a1fe0bac1..2c5ca0b10e 100644 --- a/crates/rethnet_evm/src/state/request.rs +++ b/crates/rethnet_evm/src/state/request.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use hashbrown::HashMap; use rethnet_eth::{Address, B256, U256}; use revm::{ - db::State, + db::StateRef, primitives::{Account, AccountInfo, Bytecode}, DatabaseCommit, }; @@ -11,6 +11,8 @@ use tokio::sync::oneshot; use crate::state::{AccountModifierFn, StateDebug}; +use super::history::StateHistory; + /// The request type used internally by a [`SyncDatabase`]. #[derive(Debug)] pub enum Request { @@ -57,6 +59,9 @@ pub enum Request { Revert { sender: oneshot::Sender>, }, + Serialize { + sender: oneshot::Sender, + }, SetStorageSlot { address: Address, index: U256, @@ -85,7 +90,11 @@ where #[cfg_attr(feature = "tracing", tracing::instrument)] pub fn handle(self, state: &mut S) -> bool where - S: State + DatabaseCommit + StateDebug + Debug, + S: StateRef + + DatabaseCommit + + StateDebug + + StateHistory + + Debug, { match self { Request::AccountByAddress { address, sender } => { @@ -124,6 +133,7 @@ where sender.send(state.remove_snapshot(&state_root)).unwrap() } Request::Revert { sender } => sender.send(state.revert()).unwrap(), + Request::Serialize { sender } => sender.send(state.serialize()).unwrap(), Request::SetStorageSlot { address, index, diff --git a/crates/rethnet_evm/src/state/sync.rs b/crates/rethnet_evm/src/state/sync.rs index 1b09e331df..ee1e00e383 100644 --- a/crates/rethnet_evm/src/state/sync.rs +++ b/crates/rethnet_evm/src/state/sync.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, io}; use hashbrown::HashMap; use rethnet_eth::{Address, B256, U256}; use revm::{ - db::{State, StateRef}, + db::StateRef, primitives::{Account, AccountInfo, Bytecode}, DatabaseCommit, }; @@ -18,11 +18,18 @@ use tokio::{ use crate::state::{AccountModifierFn, StateDebug}; -use super::request::Request; +use super::{history::StateHistory, request::Request}; /// Trait that meets all requirements for a synchronous database that can be used by [`AsyncDatabase`]. pub trait SyncState: - State + DatabaseCommit + StateDebug + Debug + Send + Sync + 'static + StateRef + + DatabaseCommit + + StateDebug + + StateHistory + + Debug + + Send + + Sync + + 'static where E: Debug + Send, { @@ -30,7 +37,14 @@ where impl SyncState for S where - S: State + DatabaseCommit + StateDebug + Debug + Send + Sync + 'static, + S: StateRef + + DatabaseCommit + + StateDebug + + StateHistory + + Debug + + Send + + Sync + + 'static, E: Debug + Send, { } @@ -247,6 +261,18 @@ where receiver.await.unwrap() } + /// Serializes the state using ordering of addresses and storage indices. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub async fn serialize(&self) -> String { + let (sender, receiver) = oneshot::channel(); + + self.request_sender + .send(Request::Serialize { sender }) + .expect("Failed to send request"); + + receiver.await.unwrap() + } + /// Sets the storage slot at the specified address and index to the provided value. #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn set_account_storage_slot( @@ -400,6 +426,12 @@ where }) } + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn serialize(&mut self) -> String { + task::block_in_place(move || self.runtime.block_on(AsyncState::serialize(*self))) + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] fn set_account_storage_slot( &mut self, address: Address, @@ -417,6 +449,13 @@ where fn state_root(&mut self) -> Result { task::block_in_place(move || self.runtime.block_on(AsyncState::state_root(*self))) } +} + +impl<'d, E> StateHistory for &'d AsyncState +where + E: Debug + Send + 'static, +{ + type Error = E; #[cfg_attr(feature = "tracing", tracing::instrument)] fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { diff --git a/crates/rethnet_evm/src/state/trie.rs b/crates/rethnet_evm/src/state/trie.rs new file mode 100644 index 0000000000..bb29da80e5 --- /dev/null +++ b/crates/rethnet_evm/src/state/trie.rs @@ -0,0 +1,234 @@ +mod account; + +pub use self::account::AccountTrie; + +use hashbrown::HashMap; +use rethnet_eth::{ + account::{BasicAccount, KECCAK_EMPTY}, + trie::KECCAK_NULL_RLP, + Address, B160, B256, U256, +}; +use revm::{ + db::StateRef, + primitives::{Account, AccountInfo, Bytecode}, + DatabaseCommit, +}; + +use super::{ + contract::ContractStorage, layered::LayeredChanges, RethnetLayer, StateDebug, StateError, +}; + +/// An implementation of revm's state that uses a trie. +#[derive(Clone, Debug, Default)] +pub struct TrieState { + accounts: AccountTrie, + contracts: ContractStorage, +} + +impl TrieState { + /// Constructs a [`TrieState`] from the provided [`AccountTrie`]. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn with_accounts(accounts: AccountTrie) -> Self { + Self { + accounts, + ..TrieState::default() + } + } + + /// Inserts the provided bytecode using its hash, potentially overwriting an existing value. + pub fn insert_code(&mut self, code: Bytecode) { + self.contracts.insert_code(code); + } + + /// Removes the code corresponding to the provided hash, if it exists. + pub fn remove_code(&mut self, code_hash: &B256) { + if *code_hash != KECCAK_EMPTY { + self.contracts.remove_code(code_hash); + } + } +} + +impl StateRef for TrieState { + type Error = StateError; + + fn basic(&self, address: Address) -> Result, Self::Error> { + Ok(self.accounts.account(&address).map(AccountInfo::from)) + } + + fn code_by_hash(&self, code_hash: B256) -> Result { + self.contracts + .get(&code_hash) + .cloned() + .ok_or(StateError::InvalidCodeHash(code_hash)) + } + + fn storage(&self, address: B160, index: U256) -> Result { + Ok(self + .accounts + .account_storage_slot(&address, &index) + .unwrap_or(U256::ZERO)) + } +} + +impl DatabaseCommit for TrieState { + fn commit(&mut self, mut changes: HashMap) { + changes.iter_mut().for_each(|(address, account)| { + if account.is_destroyed { + self.remove_code(&account.info.code_hash); + } else if account.is_empty() { + // Don't do anything. Account was merely touched + } else { + let old_code_hash = self + .accounts + .account(address) + .map_or(KECCAK_EMPTY, |old_account| old_account.code_hash); + + let code_changed = old_code_hash != account.info.code_hash; + if code_changed { + if let Some(new_code) = account.info.code.take() { + self.insert_code(new_code); + } + + self.remove_code(&old_code_hash); + } + } + }); + + self.accounts.commit(&changes); + } +} + +impl StateDebug for TrieState { + type Error = StateError; + + fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { + Ok(self.accounts.storage_root(address)) + } + + fn insert_account( + &mut self, + address: Address, + mut account_info: AccountInfo, + ) -> Result<(), Self::Error> { + if let Some(code) = account_info.code.take() { + account_info.code_hash = code.hash; + + self.insert_code(code); + } + + self.accounts.set_account(&address, &account_info); + + Ok(()) + } + + fn modify_account( + &mut self, + address: Address, + modifier: super::AccountModifierFn, + ) -> Result<(), Self::Error> { + let mut account_info = self.accounts.account(&address).map_or_else( + || AccountInfo { + code: None, + ..AccountInfo::default() + }, + |account| { + let mut account_info = AccountInfo::from(account); + + // Fill the bytecode + if account_info.code_hash != KECCAK_EMPTY { + account_info.code = Some( + self.code_by_hash(account_info.code_hash) + .expect("Code must exist"), + ); + } + + account_info + }, + ); + + let old_code_hash = account_info.code_hash; + + modifier( + &mut account_info.balance, + &mut account_info.nonce, + &mut account_info.code, + ); + + // Strip the bytecode + let new_code = account_info.code.take(); + + let new_code_hash = new_code.as_ref().map_or(KECCAK_EMPTY, |code| code.hash()); + account_info.code_hash = new_code_hash; + + let code_changed = new_code_hash != old_code_hash; + if code_changed { + if let Some(new_code) = new_code { + self.insert_code(new_code); + } + + self.remove_code(&old_code_hash); + } + + self.accounts.set_account(&address, &account_info); + + Ok(()) + } + + fn remove_account(&mut self, address: Address) -> Result, Self::Error> { + Ok(self.accounts.remove_account(&address).map(|account| { + self.remove_code(&account.code_hash); + + AccountInfo { + balance: account.balance, + nonce: account.nonce, + code_hash: account.code_hash, + code: None, + } + })) + } + + fn serialize(&mut self) -> String { + self.accounts.serialize() + } + + fn set_account_storage_slot( + &mut self, + address: Address, + index: U256, + value: U256, + ) -> Result<(), Self::Error> { + self.accounts + .set_account_storage_slot(&address, &index, &value); + + Ok(()) + } + + fn state_root(&mut self) -> Result { + Ok(self.accounts.state_root()) + } +} + +impl From<&LayeredChanges> for TrieState { + fn from(changes: &LayeredChanges) -> Self { + let accounts = AccountTrie::from_changes(changes.rev().map(|layer| { + layer.accounts().map(|(address, account)| { + ( + address, + account.as_ref().map(|account| { + ( + BasicAccount::from((&account.info, KECCAK_NULL_RLP)), + &account.storage, + ) + }), + ) + }) + })); + + let contracts = ContractStorage::from(changes); + + Self { + accounts, + contracts, + } + } +} diff --git a/crates/rethnet_evm/src/state/trie/account.rs b/crates/rethnet_evm/src/state/trie/account.rs new file mode 100644 index 0000000000..3c2e89253e --- /dev/null +++ b/crates/rethnet_evm/src/state/trie/account.rs @@ -0,0 +1,658 @@ +use std::{collections::BTreeMap, fmt::Debug, sync::Arc}; + +use cita_trie::{MemoryDB, PatriciaTrie, Trie as CitaTrie}; +use hashbrown::HashMap; +use hasher::{Hasher, HasherKeccak}; +use rethnet_eth::{account::BasicAccount, Address, B160, B256, U256}; +use revm::primitives::{Account, AccountInfo}; + +/// A change to the account, where `None` implies deletion. +pub type AccountChange<'a> = (&'a Address, Option<(BasicAccount, &'a HashMap)>); + +type AccountStorageTries = HashMap, B256)>; + +type Trie = PatriciaTrie; + +/// A trie for maintaining the state of accounts and their storage. +#[derive(Debug)] +pub struct AccountTrie { + state_root: B256, + state_trie_db: Arc, + storage_trie_dbs: AccountStorageTries, +} + +impl AccountTrie { + /// Constructs a `TrieState` from an (address -> account) mapping. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn with_accounts(accounts: &HashMap) -> Self { + let state_trie_db = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + + let mut storage_trie_dbs = HashMap::new(); + + let state_root = { + let mut state_trie = Trie::new(state_trie_db.clone(), hasher.clone()); + accounts.iter().for_each(|(address, account_info)| { + let storage_trie_db = Arc::new(MemoryDB::new(true)); + let storage_root = { + let mut storage_trie = Trie::new(storage_trie_db.clone(), hasher.clone()); + + B256::from_slice(&storage_trie.root().unwrap()) + }; + storage_trie_dbs.insert(*address, (storage_trie_db, storage_root)); + + Self::set_account_in(address, account_info, storage_root, &mut state_trie); + }); + + B256::from_slice(&state_trie.root().unwrap()) + }; + + Self { + state_root, + state_trie_db, + storage_trie_dbs, + } + } + + /// Constructs a `TrieState` from layers of changes. + #[cfg_attr(feature = "tracing", tracing::instrument(skip(layers)))] + pub fn from_changes<'a, I, C>(layers: I) -> Self + where + I: IntoIterator, + C: IntoIterator>, + { + let state_trie_db = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + + let mut storage_trie_dbs = HashMap::new(); + + let state_root = { + let mut state_trie = Trie::new(state_trie_db.clone(), hasher.clone()); + + layers.into_iter().for_each(|layer| { + layer.into_iter().for_each(|(address, change)| { + if let Some((mut account, storage)) = change { + let storage_trie_db = Arc::new(MemoryDB::new(true)); + + let storage_root = { + let mut storage_trie = + Trie::new(storage_trie_db.clone(), hasher.clone()); + + storage.iter().for_each(|(index, value): (&U256, &U256)| { + Self::set_account_storage_slot_in(index, value, &mut storage_trie); + }); + + B256::from_slice(&storage_trie.root().unwrap()) + }; + + // Overwrites any existing storage in the process, as we receive the complete storage every change + storage_trie_dbs.insert(*address, (storage_trie_db, storage_root)); + + account.storage_root = storage_root; + + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + state_trie + .insert(hashed_address, rlp::encode(&account).to_vec()) + .unwrap(); + } else { + Self::remove_account_in(address, &mut state_trie, &mut storage_trie_dbs); + } + }) + }); + + B256::from_slice(&state_trie.root().unwrap()) + }; + + Self { + state_root, + state_trie_db, + storage_trie_dbs, + } + } + + /// Retrieves an account corresponding to the specified address from the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn account(&self, address: &Address) -> Option { + let state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + Self::account_in(address, &state_trie) + } + + fn account_in(address: &Address, state_trie: &Trie) -> Option { + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + + state_trie + .get(&hashed_address) + .unwrap() + .map(|encoded_account| rlp::decode::(&encoded_account).unwrap()) + } + + /// Retrieves the storage storage corresponding to the account at the specified address and the specified index, if they exist. + pub fn account_storage_slot(&self, address: &Address, index: &U256) -> Option { + self.storage_trie_dbs + .get(address) + .and_then(|(storage_trie_db, storage_root)| { + let storage_trie = Trie::from( + storage_trie_db.clone(), + Arc::new(HasherKeccak::new()), + storage_root.as_bytes(), + ) + .expect("Invalid storage root"); + + let hashed_index = HasherKeccak::new().digest(&index.to_be_bytes::<32>()); + storage_trie + .get(&hashed_index) + .unwrap() + .map(|decode_value| rlp::decode::(&decode_value).unwrap()) + }) + } + + /// Commits changes to the state. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn commit(&mut self, changes: &HashMap) { + let mut state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + changes.iter().for_each(|(address, account)| { + if account.is_destroyed || account.is_empty() { + // Removes account only if it exists, so safe to use for empty, touched accounts + Self::remove_account_in(address, &mut state_trie, &mut self.storage_trie_dbs); + } else { + if account.storage_cleared { + // We can simply remove the storage trie db, as it will get reinitialized in the next operation + self.storage_trie_dbs.remove(address); + } + + let (storage_trie_db, storage_root) = + self.storage_trie_dbs.entry(*address).or_insert_with(|| { + let storage_trie_db = Arc::new(MemoryDB::new(true)); + let storage_root = { + let mut storage_trie = + Trie::new(storage_trie_db.clone(), Arc::new(HasherKeccak::new())); + + B256::from_slice(&storage_trie.root().unwrap()) + }; + + (storage_trie_db, storage_root) + }); + + let storage_changed = account.storage_cleared || !account.storage.is_empty(); + if storage_changed { + let mut storage_trie = Trie::from( + storage_trie_db.clone(), + Arc::new(HasherKeccak::new()), + storage_root.as_bytes(), + ) + .expect("Invalid storage root"); + + account.storage.iter().for_each(|(index, value)| { + Self::set_account_storage_slot_in( + index, + &value.present_value, + &mut storage_trie, + ); + }); + + *storage_root = B256::from_slice(&storage_trie.root().unwrap()); + } + + Self::set_account_in(address, &account.info, *storage_root, &mut state_trie); + } + }); + + self.state_root = B256::from_slice(&state_trie.root().unwrap()); + } + + /// Sets the provided account at the specified address. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn set_account(&mut self, address: &Address, account_info: &AccountInfo) { + let mut state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + // Check whether the account already existed. If so, use its storage root. + let (_db, storage_root) = self.storage_trie_dbs.entry(*address).or_insert_with(|| { + let storage_trie_db = Arc::new(MemoryDB::new(true)); + let storage_root = { + let mut storage_trie = + Trie::new(storage_trie_db.clone(), Arc::new(HasherKeccak::new())); + B256::from_slice(&storage_trie.root().unwrap()) + }; + + (storage_trie_db, storage_root) + }); + + Self::set_account_in(address, account_info, *storage_root, &mut state_trie); + + self.state_root = B256::from_slice(&state_trie.root().unwrap()); + } + + /// Helper function for setting the account at the specified address into the provided state trie. + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_account_in( + address: &Address, + account_info: &AccountInfo, + storage_root: B256, + state_trie: &mut Trie, + ) { + let account = BasicAccount::from((account_info, storage_root)); + + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + state_trie + .insert(hashed_address, rlp::encode(&account).to_vec()) + .unwrap(); + } + + /// Removes the account at the specified address, if it exists. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn remove_account(&mut self, address: &Address) -> Option { + let mut state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + let account = Self::remove_account_in(address, &mut state_trie, &mut self.storage_trie_dbs); + + self.state_root = B256::from_slice(&state_trie.root().unwrap()); + + account + } + + /// Helper function for removing the account at the specified address from the provided state trie and storage tries, if it exists. + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn remove_account_in( + address: &Address, + state_trie: &mut Trie, + storage_trie_dbs: &mut AccountStorageTries, + ) -> Option { + let account = Self::account_in(address, state_trie); + + if account.is_some() { + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + state_trie.remove(&hashed_address).unwrap(); + + storage_trie_dbs.remove(address); + } + + account + } + + /// Serializes the state using ordering of addresses and storage indices. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn serialize(&self) -> String { + let state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + #[derive(serde::Serialize)] + struct StateAccount { + /// Balance of the account. + pub balance: U256, + /// Code hash of the account. + pub code_hash: B256, + /// Nonce of the account. + pub nonce: u64, + /// Storage + pub storage: BTreeMap, + /// Storage root of the account. + pub storage_root: B256, + } + + let state: BTreeMap = self + .storage_trie_dbs + .iter() + .map(|(address, (storage_trie_db, storage_root))| { + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + let account = state_trie + .get(&hashed_address) + .unwrap() + .unwrap_or_else(|| panic!("Account with address '{}' and hashed address '{:?}' must exist in state, if a storage trie is stored for it", address, hashed_address)); + + let account: BasicAccount = rlp::decode(&account).unwrap(); + + let storage_trie = Trie::from( + storage_trie_db.clone(), + Arc::new(HasherKeccak::new()), + storage_root.as_bytes(), + ) + .expect("Invalid storage root"); + + let storage = storage_trie + .iter() + .map(|(hashed_index, encoded_value)| { + let value: U256 = rlp::decode(&encoded_value).unwrap(); + assert_eq!(hashed_index.len(), 32); + (B256::from_slice(&hashed_index), value) + }) + .collect(); + + let account = StateAccount { + balance: account.balance, + code_hash: account.code_hash, + nonce: account.nonce, + storage, + storage_root: *storage_root, + }; + + (*address, account) + }) + .collect(); + + serde_json::to_string_pretty(&state).unwrap() + } + + /// Sets the storage slot at the specified address and index to the provided value. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn set_account_storage_slot(&mut self, address: &Address, index: &U256, value: &U256) { + let (storage_trie_db, storage_root) = + self.storage_trie_dbs.entry(*address).or_insert_with(|| { + let storage_trie_db = Arc::new(MemoryDB::new(true)); + let storage_root = { + let mut storage_trie = + Trie::new(storage_trie_db.clone(), Arc::new(HasherKeccak::new())); + B256::from_slice(&storage_trie.root().unwrap()) + }; + + (storage_trie_db, storage_root) + }); + + { + let mut storage_trie = Trie::from( + storage_trie_db.clone(), + Arc::new(HasherKeccak::new()), + storage_root.as_bytes(), + ) + .expect("Invalid storage root"); + + Self::set_account_storage_slot_in(index, value, &mut storage_trie); + + *storage_root = B256::from_slice(&storage_trie.root().unwrap()); + }; + + let mut state_trie = Trie::from( + self.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + self.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + let hashed_address = HasherKeccak::new().digest(address.as_bytes()); + let account = state_trie.get(&hashed_address).unwrap().map_or( + BasicAccount { + storage_root: *storage_root, + ..BasicAccount::default() + }, + |account| { + let mut account: BasicAccount = rlp::decode(&account).unwrap(); + account.storage_root = *storage_root; + account + }, + ); + + state_trie + .insert(hashed_address, rlp::encode(&account).to_vec()) + .unwrap(); + + self.state_root = B256::from_slice(&state_trie.root().unwrap()); + } + + /// Helper function for setting the storage slot at the specified address and index to the provided value. + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn set_account_storage_slot_in(index: &U256, value: &U256, storage_trie: &mut Trie) { + let hashed_index = HasherKeccak::new().digest(&index.to_be_bytes::<32>()); + if *value == U256::ZERO { + if storage_trie.contains(&hashed_index).unwrap() { + storage_trie.remove(&hashed_index).unwrap(); + } + } else { + storage_trie + .insert(hashed_index, rlp::encode(value).to_vec()) + .unwrap(); + } + } + + /// Retrieves the trie's state root. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn state_root(&self) -> B256 { + self.state_root + } + + /// Retrieves the storage root of the account at the specified address. + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn storage_root(&self, address: &Address) -> Option { + self.storage_trie_dbs.get(address).map(|(_db, root)| *root) + } +} + +impl Clone for AccountTrie { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn clone(&self) -> Self { + let state_trie_db = Arc::new((*self.state_trie_db).clone()); + + let storage_trie_dbs = self + .storage_trie_dbs + .iter() + .map(|(address, (storage_trie_db, storage_root))| { + let storage_trie_db = Arc::new((**storage_trie_db).clone()); + + (*address, (storage_trie_db, *storage_root)) + }) + .collect(); + + Self { + state_root: self.state_root, + state_trie_db, + storage_trie_dbs, + } + } +} + +impl Default for AccountTrie { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn default() -> Self { + let state_trie_db = Arc::new(MemoryDB::new(true)); + let state_root = { + let mut state_trie = Trie::new(state_trie_db.clone(), Arc::new(HasherKeccak::new())); + + B256::from_slice(&state_trie.root().unwrap()) + }; + + Self { + state_root, + state_trie_db, + storage_trie_dbs: HashMap::new(), + } + } +} + +#[cfg(test)] +mod tests { + use rethnet_eth::{ + account::KECCAK_EMPTY, + state::{state_root, Storage}, + trie::KECCAK_NULL_RLP, + }; + + use super::*; + + fn precompiled_contracts() -> HashMap { + let mut accounts = HashMap::new(); + + // Mimic precompiles activation + for idx in 1..=8 { + let mut address = Address::zero(); + address.0[19] = idx; + accounts.insert(address, AccountInfo::default()); + } + + accounts + } + + #[test] + fn clone_empty() { + let state = AccountTrie::default(); + let cloned_state = state.clone(); + + assert_eq!(state.state_root(), cloned_state.state_root()); + } + + #[test] + fn clone_precompiles() { + let accounts = precompiled_contracts(); + + let state = AccountTrie::with_accounts(&accounts); + let cloned_state = state.clone(); + + assert_eq!(state.state_root(), cloned_state.state_root()); + } + + #[test] + fn default_empty() { + let state = AccountTrie::default(); + + assert_eq!(state.state_root(), KECCAK_NULL_RLP); + } + + #[test] + fn with_accounts_empty() { + let accounts = HashMap::new(); + let state = AccountTrie::with_accounts(&accounts); + + assert_eq!(state.state_root(), KECCAK_NULL_RLP); + } + + #[test] + fn with_accounts_precompiles() { + let accounts = precompiled_contracts(); + + let old: HashMap<_, _> = accounts + .iter() + .map(|(address, account_info)| { + ( + *address, + BasicAccount { + nonce: account_info.nonce, + balance: account_info.balance, + storage_root: KECCAK_NULL_RLP, + code_hash: account_info.code_hash, + }, + ) + }) + .collect(); + + let old = state_root(old.iter()); + + let state = AccountTrie::with_accounts(&accounts); + + assert_eq!(state.state_root(), old); + } + + #[test] + fn from_changes_empty() { + let changes: Vec>> = Vec::new(); + let state = AccountTrie::from_changes(changes); + + assert_eq!(state.state_root(), KECCAK_NULL_RLP); + } + + #[test] + fn from_changes_one_layer() { + const DUMMY_ADDRESS: [u8; 20] = [1u8; 20]; + + let expected_address = Address::from(DUMMY_ADDRESS); + let expected_storage = Storage::new(); + + let expected_account = BasicAccount { + nonce: 1, + balance: U256::from(100u32), + storage_root: KECCAK_NULL_RLP, + code_hash: KECCAK_EMPTY, + }; + + let changes: Vec>> = vec![vec![( + &expected_address, + Some((expected_account.clone(), &expected_storage)), + )]]; + let state = AccountTrie::from_changes(changes); + + let state_trie = Trie::from( + state.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + state.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + let account = state_trie + .get(&HasherKeccak::new().digest(expected_address.as_bytes())) + .unwrap() + .expect("Account must exist"); + + let account: BasicAccount = rlp::decode(&account).expect("Failed to decode account"); + + assert_eq!(account, expected_account); + } + + #[test] + fn from_changes_two_layers() { + const DUMMY_ADDRESS: [u8; 20] = [1u8; 20]; + + let expected_address = Address::from(DUMMY_ADDRESS); + let expected_storage = Storage::new(); + + let account_layer1 = BasicAccount { + nonce: 1, + balance: U256::from(100u32), + storage_root: KECCAK_NULL_RLP, + code_hash: KECCAK_EMPTY, + }; + + let account_layer2 = BasicAccount { + nonce: 2, + balance: U256::from(200u32), + storage_root: KECCAK_NULL_RLP, + code_hash: KECCAK_EMPTY, + }; + + let changes: Vec>> = vec![ + vec![(&expected_address, Some((account_layer1, &expected_storage)))], + vec![( + &expected_address, + Some((account_layer2.clone(), &expected_storage)), + )], + ]; + let state = AccountTrie::from_changes(changes); + + let state_trie = Trie::from( + state.state_trie_db.clone(), + Arc::new(HasherKeccak::new()), + state.state_root.as_bytes(), + ) + .expect("Invalid state root"); + + let account = state_trie + .get(&HasherKeccak::new().digest(expected_address.as_bytes())) + .unwrap() + .expect("Account must exist"); + + let account: BasicAccount = rlp::decode(&account).expect("Failed to decode account"); + + assert_eq!(account, account_layer2); + } +} diff --git a/crates/rethnet_evm_napi/src/state.rs b/crates/rethnet_evm_napi/src/state.rs index 359943838d..5892120123 100644 --- a/crates/rethnet_evm_napi/src/state.rs +++ b/crates/rethnet_evm_napi/src/state.rs @@ -10,10 +10,7 @@ use napi::{bindgen_prelude::*, JsFunction, JsObject, NapiRaw, Status}; use napi_derive::napi; use rethnet_eth::{signature::private_key_to_address, Address, Bytes, B256, U256}; use rethnet_evm::{ - state::{ - AccountModifierFn, AsyncState, LayeredState, RethnetLayer, StateDebug, StateError, - SyncState, - }, + state::{AccountModifierFn, AsyncState, HybridState, StateError, StateHistory, SyncState}, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY, }; use secp256k1::Secp256k1; @@ -100,7 +97,7 @@ impl StateManager { accounts.insert(address, AccountInfo::default()); } - let mut state = LayeredState::with_layer(RethnetLayer::with_genesis_accounts(accounts)); + let mut state = HybridState::with_accounts(accounts); state.checkpoint().unwrap(); @@ -377,6 +374,13 @@ impl StateManager { self.state.remove_snapshot(state_root).await } + /// Serializes the state using ordering of addresses and storage indices. + #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub async fn serialize(&self) -> String { + self.state.serialize().await + } + /// Sets the storage slot at the specified address and index to the provided value. #[napi] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts index 04f3873b4d..1d6eff5329 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts @@ -129,4 +129,8 @@ export class RethnetStateManager { public async setStateRoot(stateRoot: Buffer): Promise { return this._state.setStateRoot(stateRoot); } + + public async serialize(): Promise { + return this._state.serialize(); + } } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts index 065c03d1eb..1d50a10114 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts @@ -43,6 +43,9 @@ const notSupportedError = (method: string) => new Error(`${method} is not supported when forking from remote network`); export class ForkStateManager implements StateManager { + // temporary, used to print the whole storage + // should be removed + public addresses: Set = new Set(); private _state: State = ImmutableMap>(); private _initialStateRoot: string = randomHash(); private _stateRoot: string = this._initialStateRoot; @@ -154,6 +157,7 @@ export class ForkStateManager implements StateManager { } public async putContractCode(address: Address, value: Buffer): Promise { + this.addresses.add(address.toString()); const hexAddress = address.toString(); const account = (this._state.get(hexAddress) ?? makeAccountState()).set( "code", @@ -211,6 +215,7 @@ export class ForkStateManager implements StateManager { key: Buffer, value: Buffer ): Promise { + this.addresses.add(address.toString()); if (key.length !== 32) { throw new Error("Storage key must be 32 bytes long"); } @@ -241,6 +246,7 @@ export class ForkStateManager implements StateManager { } public async clearContractStorage(address: Address): Promise { + this.addresses.add(address.toString()); const hexAddress = address.toString(); let account = this._state.get(hexAddress) ?? makeAccountState(); account = account @@ -366,6 +372,7 @@ export class ForkStateManager implements StateManager { // we set an empty account instead of deleting it to avoid // re-fetching the state from the remote node. // This is only valid post spurious dragon, but we don't support older hardforks when forking. + this.addresses.add(address.toString()); const emptyAccount = makeEmptyAccountState(); this._state = this._state.set(address.toString(), emptyAccount); } @@ -391,6 +398,7 @@ export class ForkStateManager implements StateManager { } private _putAccount(address: Address, account: Account): void { + this.addresses.add(address.toString()); // Because the vm only ever modifies the nonce, balance and codeHash using this // method we ignore the stateRoot property const hexAddress = address.toString(); @@ -429,6 +437,7 @@ export class ForkStateManager implements StateManager { address: Address, accountFields: any ): Promise { + this.addresses.add(address.toString()); // copied from BaseStateManager const account = await this.getAccount(address); account.nonce = accountFields.nonce ?? account.nonce; diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts index 699ffc809c..3c84c377bd 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts @@ -133,6 +133,7 @@ export class DualModeAdapter implements VMAdapter { "hex" )} !== ${rethnetRoot.toString("hex")}` ); + await this.printState(); throw new Error("Different state root"); } @@ -310,6 +311,7 @@ export class DualModeAdapter implements VMAdapter { "hex" )} !== ${rethnetRoot.toString("hex")}` ); + await this.printState(); throw new Error("Different snapshot state root"); } @@ -539,6 +541,13 @@ export class DualModeAdapter implements VMAdapter { this._ethereumJSVMTracer.clearLastError(); this._rethnetVMTracer.clearLastError(); } + + public async printState() { + console.log("EthereumJS:"); + await this._ethereumJSAdapter.printState(); + console.log("Rethnet:"); + await this._rethnetAdapter.printState(); + } } function assertEqualRunTxResults( diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts index 73d28562ed..916ea5aea1 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts @@ -38,12 +38,73 @@ import { FakeSenderTransaction } from "../transactions/FakeSenderTransaction"; import { HardhatBlockchainInterface } from "../types/HardhatBlockchainInterface"; import { Bloom } from "../utils/bloom"; import { makeForkClient } from "../utils/makeForkClient"; +import { makeAccount } from "../utils/makeAccount"; import { makeStateTrie } from "../utils/makeStateTrie"; import { Exit } from "./exit"; import { RunTxResult, Trace, VMAdapter } from "./vm-adapter"; /* eslint-disable @nomiclabs/hardhat-internal-rules/only-hardhat-error */ +// temporary wrapper class used to print the whole storage +class DefaultStateManagerWithAddresses extends DefaultStateManager { + public addresses: Set = new Set(); + + public putAccount(address: Address, account: Account): Promise { + this.addresses.add(address.toString()); + return super.putAccount(address, account); + } + + public deleteAccount(address: Address): Promise { + this.addresses.add(address.toString()); + return super.deleteAccount(address); + } + + public modifyAccountFields( + address: Address, + accountFields: any + ): Promise { + this.addresses.add(address.toString()); + return super.modifyAccountFields(address, accountFields); + } + + public putContractCode(address: Address, value: Buffer): Promise { + this.addresses.add(address.toString()); + return super.putContractCode(address, value); + } + + public putContractStorage( + address: Address, + key: Buffer, + value: Buffer + ): Promise { + this.addresses.add(address.toString()); + return super.putContractStorage(address, key, value); + } + + public clearContractStorage(address: Address): Promise { + this.addresses.add(address.toString()); + return super.clearContractStorage(address); + } +} + +interface Storage { + [address: string]: { + balance: string; + nonce: number; + // eslint-disable-next-line @typescript-eslint/naming-convention + code_hash: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + storage_root: string; + storage: { + [storageSlot: string]: string; + }; + }; +} + +type StateManagerWithAddresses = StateManager & { + addresses: Set; +}; + export class EthereumJSAdapter implements VMAdapter { private _blockStartStateRoot: Buffer | undefined; @@ -51,7 +112,7 @@ export class EthereumJSAdapter implements VMAdapter { constructor( private readonly _vm: VM, - private readonly _stateManager: StateManager, + public readonly _stateManager: StateManagerWithAddresses, private readonly _blockchain: HardhatBlockchainInterface, private readonly _common: Common, private readonly _configNetworkId: number, @@ -84,7 +145,7 @@ export class EthereumJSAdapter implements VMAdapter { config: NodeConfig, selectHardfork: (blockNumber: bigint) => string ): Promise { - let stateManager: StateManager; + let stateManager: StateManagerWithAddresses; let forkBlockNum: bigint | undefined; let forkNetworkId: number | undefined; @@ -107,9 +168,14 @@ export class EthereumJSAdapter implements VMAdapter { } else { const stateTrie = await makeStateTrie(config.genesisAccounts); - stateManager = new DefaultStateManager({ + stateManager = new DefaultStateManagerWithAddresses({ trie: stateTrie, }); + + for (const genesisAccount of config.genesisAccounts) { + const { address } = makeAccount(genesisAccount); + stateManager.addresses.add(address.toString()); + } } const eei = new EEI(stateManager, common, blockchain); @@ -467,6 +533,80 @@ export class EthereumJSAdapter implements VMAdapter { this._vmTracer.clearLastError(); } + public async printState() { + const storage: Storage = {}; + + for (const address of this._stateManager.addresses) { + const account = await this._stateManager.getAccount( + Address.fromString(address) + ); + + const nonce = Number(account.nonce); + const balance = `0x${account.balance.toString(16).padStart(64, "0")}`; + const codeHash = `0x${account.codeHash + .toString("hex") + .padStart(64, "0")}`; + + const storageRoot = `0x${account.storageRoot + .toString("hex") + .padStart(64, "0")}`; + + const dumpedAccountStorage = await this._stateManager.dumpStorage( + Address.fromString(address) + ); + + const accountStorage: Record = {}; + + for (const [key, value] of Object.entries(dumpedAccountStorage)) { + accountStorage[`0x${key.padStart(64, "0")}`] = `0x${value.padStart( + 64, + "0" + )}`; + } + + if ( + nonce === 0 && + account.balance === 0n && + // empty code + codeHash === + "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" && + // empty storage + storageRoot === + "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + ) { + if (Object.entries(accountStorage).length > 0) { + // sanity check + throw new Error( + "Assertion error: storage root is empty but storage has data" + ); + } + + // we don't add empty accounts + continue; + } + + storage[address] = { + nonce, + balance, + code_hash: codeHash, + storage_root: storageRoot, + storage: accountStorage, + }; + } + + const replacer = (_key: any, value: any) => + typeof value === "object" && !Array.isArray(value) && value !== null + ? Object.keys(value) + .sort() + .reduce((sorted: any, key: any) => { + sorted[key] = value[key]; + return sorted; + }, {}) + : value; + + console.log(JSON.stringify(storage, replacer, 2)); + } + private _getCommonForTracing(networkId: number, blockNumber: bigint): Common { try { const common = Common.custom( diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts index b5ef329071..2d5bda6907 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts @@ -411,6 +411,10 @@ export class RethnetAdapter implements VMAdapter { this._vmTracer.clearLastError(); } + public async printState() { + console.log(await this._state.serialize()); + } + private _getBlockEnvDifficulty( difficulty: bigint | undefined ): bigint | undefined { diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts index 9dcd16b5d3..0e1085725a 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts @@ -84,4 +84,7 @@ export interface VMAdapter { // methods for snapshotting makeSnapshot(): Promise<[Buffer, boolean]>; removeSnapshot(stateRoot: Buffer): Promise; + + // for debugging purposes + printState(): Promise; } From 31457ff935af9bb99ddc0b064820b0765ff4e996 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Apr 2023 00:08:49 -0500 Subject: [PATCH 5/6] improvement: adjust napi external memory for StateManager --- crates/rethnet_evm_napi/src/state.rs | 42 ++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/rethnet_evm_napi/src/state.rs b/crates/rethnet_evm_napi/src/state.rs index 5892120123..4e601a3f0f 100644 --- a/crates/rethnet_evm_napi/src/state.rs +++ b/crates/rethnet_evm_napi/src/state.rs @@ -6,7 +6,10 @@ use std::{ }, }; -use napi::{bindgen_prelude::*, JsFunction, JsObject, NapiRaw, Status}; +use napi::{ + bindgen_prelude::{BigInt, Buffer, ObjectFinalize}, + Env, JsFunction, JsObject, NapiRaw, Status, +}; use napi_derive::napi; use rethnet_eth::{signature::private_key_to_address, Address, Bytes, B256, U256}; use rethnet_evm::{ @@ -23,6 +26,10 @@ use crate::{ threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; +// An arbitrarily large amount of memory to signal to the javascript garbage collector that it needs to +// attempt to free the state object's memory. +const STATE_MEMORY_SIZE: i64 = 10_000; + struct ModifyAccountCall { pub balance: U256, pub nonce: u64, @@ -49,7 +56,7 @@ pub struct SnapshotId { } /// The Rethnet state -#[napi] +#[napi(custom_finalize)] #[derive(Debug)] pub struct StateManager { pub(super) state: Arc>, @@ -60,14 +67,17 @@ impl StateManager { /// Constructs a [`StateManager`] with an empty state. #[napi(constructor)] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn new() -> napi::Result { - Self::with_accounts(HashMap::default()) + pub fn new(mut env: Env) -> napi::Result { + Self::with_accounts(&mut env, HashMap::default()) } /// Constructs a [`StateManager`] with the provided accounts present in the genesis state. #[napi(factory)] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn with_genesis_accounts(accounts: Vec) -> napi::Result { + pub fn with_genesis_accounts( + mut env: Env, + accounts: Vec, + ) -> napi::Result { let context = Secp256k1::signing_only(); let genesis_accounts = accounts .into_iter() @@ -85,11 +95,14 @@ impl StateManager { }) .collect::>>()?; - Self::with_accounts(genesis_accounts) + Self::with_accounts(&mut env, genesis_accounts) } #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - fn with_accounts(mut accounts: HashMap) -> napi::Result { + fn with_accounts( + env: &mut Env, + mut accounts: HashMap, + ) -> napi::Result { // Mimic precompiles activation for idx in 1..=8 { let mut address = Address::zero(); @@ -101,11 +114,11 @@ impl StateManager { state.checkpoint().unwrap(); - Self::with_state(state) + Self::with_state(env, state) } #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - fn with_state(state: S) -> napi::Result + fn with_state(env: &mut Env, state: S) -> napi::Result where S: SyncState, { @@ -115,6 +128,8 @@ impl StateManager { let state = AsyncState::new(state) .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; + env.adjust_external_memory(STATE_MEMORY_SIZE)?; + Ok(Self { state: Arc::new(state), }) @@ -412,3 +427,12 @@ impl StateManager { .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } } + +impl ObjectFinalize for StateManager { + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] + fn finalize(self, mut env: Env) -> napi::Result<()> { + env.adjust_external_memory(-STATE_MEMORY_SIZE)?; + + Ok(()) + } +} From 7e0312e7ac5555c057acd853cc9c700cdf92420f Mon Sep 17 00:00:00 2001 From: Wodann Date: Wed, 5 Apr 2023 16:29:32 -0500 Subject: [PATCH 6/6] refactor: replace background with short-lived tasks --- crates/rethnet_eth/Cargo.toml | 2 +- crates/rethnet_evm/Cargo.toml | 2 +- crates/rethnet_evm/src/block/builder.rs | 60 +-- crates/rethnet_evm/src/blockchain.rs | 19 +- crates/rethnet_evm/src/blockchain/request.rs | 49 -- crates/rethnet_evm/src/blockchain/sync.rs | 124 ----- crates/rethnet_evm/src/evm.rs | 58 +-- crates/rethnet_evm/src/inspector.rs | 47 +- crates/rethnet_evm/src/lib.rs | 4 +- crates/rethnet_evm/src/runtime.rs | 78 +-- crates/rethnet_evm/src/state.rs | 35 +- crates/rethnet_evm/src/state/request.rs | 159 ------ crates/rethnet_evm/src/state/sync.rs | 490 ------------------ crates/rethnet_evm/src/trace.rs | 11 +- crates/rethnet_evm_napi/src/blockchain.rs | 23 +- .../src/blockchain/js_blockchain.rs | 17 +- crates/rethnet_evm_napi/src/context.rs | 79 +++ crates/rethnet_evm_napi/src/lib.rs | 2 +- crates/rethnet_evm_napi/src/logger.rs | 39 -- crates/rethnet_evm_napi/src/state.rs | 227 +++++--- crates/rethnet_evm_napi/src/tracer.rs | 4 +- .../rethnet_evm_napi/src/tracer/js_tracer.rs | 58 ++- .../src/transaction/result.rs | 12 +- crates/rethnet_evm_napi/test/evm/RethnetDb.ts | 195 +++---- .../rethnet_evm_napi/test/evm/StateManager.ts | 6 +- .../src/internal/core/providers/http.ts | 2 +- .../hardhat-network/provider/RethnetState.ts | 6 +- .../hardhat-network/provider/vm/rethnet.ts | 4 + 28 files changed, 583 insertions(+), 1229 deletions(-) delete mode 100644 crates/rethnet_evm/src/blockchain/request.rs delete mode 100644 crates/rethnet_evm/src/blockchain/sync.rs delete mode 100644 crates/rethnet_evm/src/state/request.rs delete mode 100644 crates/rethnet_evm/src/state/sync.rs create mode 100644 crates/rethnet_evm_napi/src/context.rs delete mode 100644 crates/rethnet_evm_napi/src/logger.rs diff --git a/crates/rethnet_eth/Cargo.toml b/crates/rethnet_eth/Cargo.toml index cde44e5161..814d0aef3c 100644 --- a/crates/rethnet_eth/Cargo.toml +++ b/crates/rethnet_eth/Cargo.toml @@ -13,7 +13,7 @@ hex = { version = "0.4.3", default-features = false, features = ["alloc"] } open-fastrlp = { version = "0.1.2", default-features = false, features = ["derive"], optional = true } primitive-types = { version = "0.11.1", default-features = false, features = ["rlp"] } reqwest = { version = "0.11", features = ["blocking", "json"] } -revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = "1.0", default-features = false } +revm-primitives = { git = "https://github.com/Wodann/revm", rev = "a4550cc", version = "1.1", default-features = false } # revm-primitives = { path = "../../../revm/crates/primitives", version = "1.0", default-features = false } rlp = { version = "0.5.2", default-features = false, features = ["derive"] } ruint = { version = "1.7.0", default-features = false } diff --git a/crates/rethnet_evm/Cargo.toml b/crates/rethnet_evm/Cargo.toml index 0026673ffd..422d7213b8 100644 --- a/crates/rethnet_evm/Cargo.toml +++ b/crates/rethnet_evm/Cargo.toml @@ -11,7 +11,7 @@ hasher = { git = "https://github.com/Wodann/hasher", rev = "89d3fc9", version = log = { version = "0.4.17", default-features = false } parking_lot = { version = "0.12.1", default-features = false } rethnet_eth = { version = "0.1.0-dev", path = "../rethnet_eth", features = ["serde"] } -revm = { git = "https://github.com/bluealloy/revm", rev = "3789509", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } +revm = { git = "https://github.com/Wodann/revm", rev = "a4550cc", version = "3.1", default-features = false, features = ["dev", "secp256k1", "serde", "std"] } # revm = { path = "../../../revm/crates/revm", version = "3.0", default-features = false, features = ["dev", "serde", "std"] } rlp = { version = "0.5.2", default-features = false } serde = { version = "1.0.158", default-features = false, features = ["std"] } diff --git a/crates/rethnet_evm/src/block/builder.rs b/crates/rethnet_evm/src/block/builder.rs index 31c6f2dcd1..e50e1c0a8b 100644 --- a/crates/rethnet_evm/src/block/builder.rs +++ b/crates/rethnet_evm/src/block/builder.rs @@ -8,12 +8,12 @@ use revm::{ db::DatabaseComponentError, primitives::{BlockEnv, CfgEnv, EVMError, ExecutionResult, InvalidTransaction, SpecId, TxEnv}, }; -use tokio::runtime::Runtime; +use tokio::sync::RwLock; use crate::{ - blockchain::AsyncBlockchain, - evm::{run_transaction, AsyncInspector}, - state::{AccountModifierFn, AsyncState}, + blockchain::SyncBlockchain, + evm::{build_evm, run_transaction, SyncInspector}, + state::{AccountModifierFn, SyncState}, trace::Trace, HeaderData, }; @@ -51,8 +51,8 @@ where BE: Debug + Send + 'static, SE: Debug + Send + 'static, { - blockchain: Arc>, - state: Arc>, + blockchain: Arc>>>, + state: Arc>>>, header: PartialHeader, transactions: Vec, cfg: CfgEnv, @@ -65,8 +65,8 @@ where { /// Creates an intance of [`BlockBuilder`], creating a checkpoint in the process. pub fn new( - blockchain: Arc>, - state: Arc>, + blockchain: Arc>>>, + state: Arc>>>, cfg: CfgEnv, parent: Header, header: HeaderData, @@ -91,11 +91,6 @@ where } } - /// Retrieves the runtime of the [`BlockBuilder`]. - pub fn runtime(&self) -> &Runtime { - self.state.runtime() - } - /// Retrieves the amount of gas used in the block, so far. pub fn gas_used(&self) -> U256 { self.header.gas_used @@ -118,7 +113,7 @@ where pub async fn add_transaction( &mut self, transaction: TxEnv, - inspector: Option>>, + inspector: Option>>, ) -> Result<(ExecutionResult, Trace), BlockTransactionError> { // transaction's gas limit cannot be greater than the remaining gas in the block if U256::from(transaction.gas_limit) > self.gas_remaining() { @@ -140,19 +135,14 @@ where }, }; - let (result, changes, trace) = run_transaction( - self.state.runtime(), - self.blockchain.clone(), - self.state.clone(), - self.cfg.clone(), - transaction, - block, - inspector, - ) - .await - .unwrap()?; + let mut state = self.state.write().await; + let blockchain = self.blockchain.read().await; + + let evm = build_evm(&*blockchain, &*state, self.cfg.clone(), transaction, block); + + let (result, changes, trace) = run_transaction(evm, inspector)?; - self.state.apply(changes).await; + state.commit(changes); self.header.gas_used += U256::from(result.gas_used()); @@ -163,15 +153,14 @@ where /// Finalizes the block, returning the state root. /// TODO: Build a full block pub async fn finalize(self, rewards: Vec<(Address, U256)>) -> Result<(), SE> { + let mut state = self.state.write().await; for (address, reward) in rewards { - self.state - .modify_account( - address, - AccountModifierFn::new(Box::new(move |balance, _nonce, _code| { - *balance += reward; - })), - ) - .await?; + state.modify_account( + address, + AccountModifierFn::new(Box::new(move |balance, _nonce, _code| { + *balance += reward; + })), + )?; } Ok(()) @@ -179,6 +168,7 @@ where /// Aborts building of the block, reverting all transactions in the process. pub async fn abort(self) -> Result<(), SE> { - self.state.revert().await + let mut state = self.state.write().await; + state.revert() } } diff --git a/crates/rethnet_evm/src/blockchain.rs b/crates/rethnet_evm/src/blockchain.rs index 6c7d91710c..bd5279df8d 100644 --- a/crates/rethnet_evm/src/blockchain.rs +++ b/crates/rethnet_evm/src/blockchain.rs @@ -1,4 +1,17 @@ -mod request; -mod sync; +use std::fmt::Debug; -pub use sync::{AsyncBlockchain, SyncBlockchain}; +use revm::db::BlockHashRef; + +/// Trait that meets all requirements for a synchronous database that can be used by [`AsyncBlockchain`]. +pub trait SyncBlockchain: BlockHashRef + Send + Sync + Debug + 'static +where + E: Debug + Send, +{ +} + +impl SyncBlockchain for B +where + B: BlockHashRef + Send + Sync + Debug + 'static, + E: Debug + Send, +{ +} diff --git a/crates/rethnet_evm/src/blockchain/request.rs b/crates/rethnet_evm/src/blockchain/request.rs deleted file mode 100644 index a1e0cb18bf..0000000000 --- a/crates/rethnet_evm/src/blockchain/request.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fmt::Debug; - -use rethnet_eth::{B256, U256}; -use revm::db::BlockHash; -use tokio::sync::oneshot; - -/// The request type used internally by a [`SyncDatabase`]. -#[derive(Debug)] -pub enum Request -where - E: Debug, -{ - BlockHashByNumber { - number: U256, - sender: oneshot::Sender>, - }, - // InsertBlock { - // block_number: U256, - // block_hash: B256, - // sender: oneshot::Sender>, - // }, - Terminate, -} - -impl Request -where - E: Debug, -{ - pub fn handle(self, db: &mut D) -> bool - where - D: BlockHash, - { - match self { - Request::BlockHashByNumber { number, sender } => { - sender.send(db.block_hash(number)).unwrap() - } - // Request::InsertBlock { - // block_number, - // block_hash, - // sender, - // } => sender - // .send(db.insert_block(block_number, block_hash)) - // .unwrap(), - Request::Terminate => return false, - } - - true - } -} diff --git a/crates/rethnet_evm/src/blockchain/sync.rs b/crates/rethnet_evm/src/blockchain/sync.rs deleted file mode 100644 index 4bef0acfeb..0000000000 --- a/crates/rethnet_evm/src/blockchain/sync.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{fmt::Debug, io}; - -use rethnet_eth::{B256, U256}; -use revm::db::{BlockHash, BlockHashRef}; -use tokio::{ - runtime::{Builder, Runtime}, - sync::{ - mpsc::{unbounded_channel, UnboundedSender}, - oneshot, - }, - task::{self, JoinHandle}, -}; - -use super::request::Request; - -/// Trait that meets all requirements for a synchronous database that can be used by [`AsyncBlockchain`]. -pub trait SyncBlockchain: BlockHash + Send + Sync + 'static -where - E: Debug + Send, -{ -} - -impl SyncBlockchain for B -where - B: BlockHash + Send + Sync + 'static, - E: Debug + Send, -{ -} - -/// A helper class for converting a synchronous blockchain into an asynchronous blockchain. -/// -/// Requires the inner blockchain to implement [`Blockchain`]. -#[derive(Debug)] -pub struct AsyncBlockchain -where - E: Debug + Send, -{ - runtime: Runtime, - request_sender: UnboundedSender>, - blockchain_handle: Option>, -} - -impl AsyncBlockchain -where - E: Debug + Send + 'static, -{ - /// Constructs an [`AsyncBlockchain`] instance with the provided database. - pub fn new>(mut blockchain: B) -> io::Result { - let runtime = Builder::new_multi_thread().build()?; - - let (sender, mut receiver) = unbounded_channel::>(); - - let blockchain_handle = runtime.spawn(async move { - while let Some(request) = receiver.recv().await { - if !request.handle(&mut blockchain) { - break; - } - } - }); - - Ok(Self { - runtime, - request_sender: sender, - blockchain_handle: Some(blockchain_handle), - }) - } - - /// Retrieves the runtime of the [`AsyncBlockchain`]. - pub fn runtime(&self) -> &Runtime { - &self.runtime - } - - /// Retrieves the hash of the block corresponding to the specified number. - pub async fn block_hash_by_number(&self, number: U256) -> Result { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::BlockHashByNumber { number, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - // /// Inserts the specified block number and hash into the state. - // pub async fn insert_block(&self, block_number: U256, block_hash: B256) -> Result<(), E> { - // let (sender, receiver) = oneshot::channel(); - - // self.request_sender - // .send(Request::InsertBlock { - // block_number, - // block_hash, - // sender, - // }) - // .expect("Failed to send request"); - - // receiver.await.unwrap() - // } -} - -impl Drop for AsyncBlockchain -where - E: Debug + Send, -{ - fn drop(&mut self) { - if let Some(handle) = self.blockchain_handle.take() { - self.request_sender - .send(Request::Terminate) - .expect("Failed to send request"); - - self.runtime.block_on(handle).unwrap(); - } - } -} - -impl BlockHashRef for AsyncBlockchain -where - E: Debug + Send + 'static, -{ - type Error = E; - - fn block_hash(&self, number: U256) -> Result { - task::block_in_place(move || self.runtime.block_on(self.block_hash_by_number(number))) - } -} diff --git a/crates/rethnet_evm/src/evm.rs b/crates/rethnet_evm/src/evm.rs index 9cf6eadbff..b17c143bc7 100644 --- a/crates/rethnet_evm/src/evm.rs +++ b/crates/rethnet_evm/src/evm.rs @@ -1,22 +1,21 @@ -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use revm::{ db::{DatabaseComponentError, DatabaseComponents}, primitives::{BlockEnv, CfgEnv, EVMError, ExecutionResult, ResultAndState, State, TxEnv}, Inspector, }; -use tokio::{runtime::Runtime, task::JoinHandle}; use crate::{ - blockchain::AsyncBlockchain, + blockchain::SyncBlockchain, inspector::DualInspector, - runtime::AsyncDatabase, - state::AsyncState, + state::SyncState, trace::{Trace, TraceCollector}, + SyncDatabase, }; /// Super trait for an inspector of an `AsyncDatabase` that's debuggable. -pub trait AsyncInspector: Inspector> + Debug + Send +pub trait SyncInspector: Inspector> + Debug + Send where BE: Debug + Send + 'static, SE: Debug + Send + 'static, @@ -24,15 +23,14 @@ where } /// Creates an evm from the provided database, config, transaction, and block. -#[allow(clippy::type_complexity)] #[cfg_attr(feature = "tracing", tracing::instrument)] -fn build_evm( - blockchain: Arc>, - state: Arc>, +pub fn build_evm<'b, 's, BE, SE>( + blockchain: &'b dyn SyncBlockchain, + state: &'s dyn SyncState, cfg: CfgEnv, transaction: TxEnv, block: BlockEnv, -) -> revm::EVM> +) -> revm::EVM> where BE: Debug + Send + 'static, SE: Debug + Send + 'static, @@ -49,36 +47,26 @@ where evm } -#[allow(clippy::type_complexity)] -#[cfg_attr(feature = "tracing", tracing::instrument)] +#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn run_transaction( - runtime: &Runtime, - blockchain: Arc>, - state: Arc>, - cfg: CfgEnv, - transaction: TxEnv, - block: BlockEnv, - inspector: Option>>, -) -> JoinHandle>>> + evm: revm::EVM>, + inspector: Option>>, +) -> Result<(ExecutionResult, State, Trace), EVMError>> where BE: Debug + Send + 'static, SE: Debug + Send + 'static, { - runtime.spawn(async move { - let mut evm = build_evm(blockchain, state, cfg, transaction, block); - - let (result, state, tracer) = if let Some(inspector) = inspector { - let mut inspector = DualInspector::new(TraceCollector::default(), inspector); + let (result, state, tracer) = if let Some(inspector) = inspector { + let mut inspector = DualInspector::new(TraceCollector::default(), inspector); - let ResultAndState { result, state } = evm.inspect(&mut inspector)?; - (result, state, inspector.into_parts().0) - } else { - let mut inspector = TraceCollector::default(); - let ResultAndState { result, state } = evm.inspect(&mut inspector)?; + let ResultAndState { result, state } = evm.inspect_ref(&mut inspector)?; + (result, state, inspector.into_parts().0) + } else { + let mut inspector = TraceCollector::default(); + let ResultAndState { result, state } = evm.inspect_ref(&mut inspector)?; - (result, state, inspector) - }; + (result, state, inspector) + }; - Ok((result, state, tracer.into_trace())) - }) + Ok((result, state, tracer.into_trace())) } diff --git a/crates/rethnet_evm/src/inspector.rs b/crates/rethnet_evm/src/inspector.rs index c8f99c9014..8c7bc40eed 100644 --- a/crates/rethnet_evm/src/inspector.rs +++ b/crates/rethnet_evm/src/inspector.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use revm::{Database, Inspector}; +use revm::Inspector; // TODO: Improve this design by introducing a InspectorMut trait @@ -8,22 +8,20 @@ use revm::{Database, Inspector}; /// first, followed by the mutable inspector. To ensure both inspectors observe a valid state, you /// have to ensure that only the mutable inspector modifies state. The returned values are solely /// determined by the mutable inspector. -pub struct DualInspector +pub struct DualInspector where - A: Inspector, - B: Inspector, - DB: Database, + A: Inspector, + B: Inspector, { immutable: A, mutable: B, - phantom: PhantomData, + phantom: PhantomData, } -impl DualInspector +impl DualInspector where - A: Inspector, - B: Inspector, - DB: Database, + A: Inspector, + B: Inspector, { /// Constructs a `DualInspector` from the provided inspectors. pub fn new(immutable: A, mutable: B) -> Self { @@ -40,16 +38,15 @@ where } } -impl Inspector for DualInspector +impl Inspector for DualInspector where - A: Inspector, - B: Inspector, - DB: Database, + A: Inspector, + B: Inspector, { fn initialize_interp( &mut self, interp: &mut revm::interpreter::Interpreter, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, is_static: bool, ) -> revm::interpreter::InstructionResult { self.immutable.initialize_interp(interp, data, is_static); @@ -59,7 +56,7 @@ where fn step( &mut self, interp: &mut revm::interpreter::Interpreter, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, is_static: bool, ) -> revm::interpreter::InstructionResult { self.immutable.step(interp, data, is_static); @@ -68,7 +65,7 @@ where fn log( &mut self, - evm_data: &mut revm::EVMData<'_, DB>, + evm_data: &mut dyn revm::EVMData, address: &rethnet_eth::B160, topics: &[rethnet_eth::B256], data: &rethnet_eth::Bytes, @@ -80,7 +77,7 @@ where fn step_end( &mut self, interp: &mut revm::interpreter::Interpreter, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, is_static: bool, eval: revm::interpreter::InstructionResult, ) -> revm::interpreter::InstructionResult { @@ -90,7 +87,7 @@ where fn call( &mut self, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, inputs: &mut revm::interpreter::CallInputs, is_static: bool, ) -> ( @@ -104,7 +101,7 @@ where fn call_end( &mut self, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, inputs: &revm::interpreter::CallInputs, remaining_gas: revm::interpreter::Gas, ret: revm::interpreter::InstructionResult, @@ -123,7 +120,7 @@ where fn create( &mut self, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, inputs: &mut revm::interpreter::CreateInputs, ) -> ( revm::interpreter::InstructionResult, @@ -137,7 +134,7 @@ where fn create_end( &mut self, - data: &mut revm::EVMData<'_, DB>, + data: &mut dyn revm::EVMData, inputs: &revm::interpreter::CreateInputs, ret: revm::interpreter::InstructionResult, address: Option, @@ -155,8 +152,8 @@ where .create_end(data, inputs, ret, address, remaining_gas, out) } - fn selfdestruct(&mut self) { - self.immutable.selfdestruct(); - self.mutable.selfdestruct(); + fn selfdestruct(&mut self, contract: rethnet_eth::B160, target: rethnet_eth::B160) { + self.immutable.selfdestruct(contract, target); + self.mutable.selfdestruct(contract, target); } } diff --git a/crates/rethnet_evm/src/lib.rs b/crates/rethnet_evm/src/lib.rs index d89b9fb640..514eba5ff6 100644 --- a/crates/rethnet_evm/src/lib.rs +++ b/crates/rethnet_evm/src/lib.rs @@ -21,8 +21,8 @@ pub use revm::{ pub use crate::{ block::{BlockBuilder, HeaderData}, - evm::AsyncInspector, - runtime::{AsyncDatabase, Rethnet}, + evm::SyncInspector, + runtime::{Rethnet, SyncDatabase}, transaction::{PendingTransaction, TransactionError}, }; diff --git a/crates/rethnet_evm/src/runtime.rs b/crates/rethnet_evm/src/runtime.rs index a135fada88..df273e8e62 100644 --- a/crates/rethnet_evm/src/runtime.rs +++ b/crates/rethnet_evm/src/runtime.rs @@ -4,18 +4,20 @@ use revm::{ db::DatabaseComponents, primitives::{BlockEnv, CfgEnv, ExecutionResult, SpecId, TxEnv}, }; +use tokio::sync::RwLock; use crate::{ - blockchain::AsyncBlockchain, - evm::{run_transaction, AsyncInspector}, - state::AsyncState, + blockchain::SyncBlockchain, + evm::{build_evm, run_transaction, SyncInspector}, + state::SyncState, trace::Trace, transaction::TransactionError, State, }; /// Asynchronous implementation of the Database super-trait -pub type AsyncDatabase = DatabaseComponents>, Arc>>; +pub type SyncDatabase<'b, 's, BE, SE> = + DatabaseComponents<&'s dyn SyncState, &'b dyn SyncBlockchain>; /// The asynchronous Rethnet runtime. #[derive(Debug)] @@ -24,8 +26,8 @@ where BE: Debug + Send + 'static, SE: Debug + Send + 'static, { - blockchain: Arc>, - state: Arc>, + blockchain: Arc>>>, + state: Arc>>>, cfg: CfgEnv, } @@ -35,10 +37,14 @@ where SE: Debug + Send + 'static, { /// Constructs a new [`Rethnet`] instance. - pub fn new(blockchain: Arc>, db: Arc>, cfg: CfgEnv) -> Self { + pub fn new( + blockchain: Arc>>>, + state: Arc>>>, + cfg: CfgEnv, + ) -> Self { Self { blockchain, - state: db, + state, cfg, } } @@ -49,24 +55,18 @@ where &self, transaction: TxEnv, block: BlockEnv, - inspector: Option>>, + inspector: Option>>, ) -> Result<(ExecutionResult, State, Trace), TransactionError> { if self.cfg.spec_id > SpecId::MERGE && block.prevrandao.is_none() { return Err(TransactionError::MissingPrevrandao); } - run_transaction( - self.state.runtime(), - self.blockchain.clone(), - self.state.clone(), - self.cfg.clone(), - transaction, - block, - inspector, - ) - .await - .unwrap() - .map_err(TransactionError::from) + let state = self.state.read().await; + let blockchain = self.blockchain.read().await; + + let evm = build_evm(&*blockchain, &*state, self.cfg.clone(), transaction, block); + + run_transaction(evm, inspector).map_err(TransactionError::from) } /// Runs a transaction without committing the state, while disabling balance checks and creating accounts for new addresses. @@ -75,7 +75,7 @@ where &self, transaction: TxEnv, block: BlockEnv, - inspector: Option>>, + inspector: Option>>, ) -> Result<(ExecutionResult, State, Trace), TransactionError> { if self.cfg.spec_id > SpecId::MERGE && block.prevrandao.is_none() { return Err(TransactionError::MissingPrevrandao); @@ -84,18 +84,12 @@ where let mut cfg = self.cfg.clone(); cfg.disable_balance_check = true; - run_transaction( - self.state.runtime(), - self.blockchain.clone(), - self.state.clone(), - cfg, - transaction, - block, - inspector, - ) - .await - .unwrap() - .map_err(TransactionError::from) + let state = self.state.read().await; + let blockchain = self.blockchain.read().await; + + let evm = build_evm(&*blockchain, &*state, cfg, transaction, block); + + run_transaction(evm, inspector).map_err(TransactionError::from) } /// Runs a transaction, committing the state in the process. @@ -104,11 +98,21 @@ where &self, transaction: TxEnv, block: BlockEnv, - inspector: Option>>, + inspector: Option>>, ) -> Result<(ExecutionResult, Trace), TransactionError> { - let (result, changes, trace) = self.dry_run(transaction, block, inspector).await?; + if self.cfg.spec_id > SpecId::MERGE && block.prevrandao.is_none() { + return Err(TransactionError::MissingPrevrandao); + } + + let mut state = self.state.write().await; + let blockchain = self.blockchain.read().await; + + let evm = build_evm(&*blockchain, &*state, self.cfg.clone(), transaction, block); + + let (result, changes, trace) = + run_transaction(evm, inspector).map_err(TransactionError::from)?; - self.state.apply(changes).await; + state.commit(changes); Ok((result, trace)) } diff --git a/crates/rethnet_evm/src/state.rs b/crates/rethnet_evm/src/state.rs index c86606cfb7..4eb3a03ca5 100644 --- a/crates/rethnet_evm/src/state.rs +++ b/crates/rethnet_evm/src/state.rs @@ -5,11 +5,12 @@ mod history; mod hybrid; mod layered; mod remote; -mod request; -mod sync; mod trie; +use std::fmt::Debug; + use rethnet_eth::B256; +use revm::{db::StateRef, DatabaseCommit}; pub use self::{ debug::{AccountModifierFn, StateDebug}, @@ -17,7 +18,6 @@ pub use self::{ hybrid::HybridState, layered::{LayeredState, RethnetLayer}, remote::RemoteDatabase, - sync::{AsyncState, SyncState}, }; /// Combinatorial error for the database API @@ -33,3 +33,32 @@ pub enum StateError { #[error("State root `{0}` does not exist.")] InvalidStateRoot(B256), } + +/// Trait that meets all requirements for a synchronous database that can be used by [`AsyncDatabase`]. +pub trait SyncState: + StateRef + + DatabaseCommit + + StateDebug + + StateHistory + + Debug + + Send + + Sync + + 'static +where + E: Debug + Send, +{ +} + +impl SyncState for S +where + S: StateRef + + DatabaseCommit + + StateDebug + + StateHistory + + Debug + + Send + + Sync + + 'static, + E: Debug + Send, +{ +} diff --git a/crates/rethnet_evm/src/state/request.rs b/crates/rethnet_evm/src/state/request.rs deleted file mode 100644 index 2c5ca0b10e..0000000000 --- a/crates/rethnet_evm/src/state/request.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::fmt::Debug; - -use hashbrown::HashMap; -use rethnet_eth::{Address, B256, U256}; -use revm::{ - db::StateRef, - primitives::{Account, AccountInfo, Bytecode}, - DatabaseCommit, -}; -use tokio::sync::oneshot; - -use crate::state::{AccountModifierFn, StateDebug}; - -use super::history::StateHistory; - -/// The request type used internally by a [`SyncDatabase`]. -#[derive(Debug)] -pub enum Request { - AccountByAddress { - address: Address, - sender: oneshot::Sender, E>>, - }, - AccountStorageRoot { - address: Address, - sender: oneshot::Sender, E>>, - }, - Checkpoint { - sender: oneshot::Sender>, - }, - CodeByHash { - code_hash: B256, - sender: oneshot::Sender>, - }, - Commit { - changes: HashMap, - sender: oneshot::Sender<()>, - }, - InsertAccount { - address: Address, - account_info: AccountInfo, - sender: oneshot::Sender>, - }, - MakeSnapshot { - sender: oneshot::Sender<(B256, bool)>, - }, - ModifyAccount { - address: Address, - modifier: AccountModifierFn, - sender: oneshot::Sender>, - }, - RemoveAccount { - address: Address, - sender: oneshot::Sender, E>>, - }, - RemoveSnapshot { - state_root: B256, - sender: oneshot::Sender, - }, - Revert { - sender: oneshot::Sender>, - }, - Serialize { - sender: oneshot::Sender, - }, - SetStorageSlot { - address: Address, - index: U256, - value: U256, - sender: oneshot::Sender>, - }, - SetStateRoot { - state_root: B256, - sender: oneshot::Sender>, - }, - StateRoot { - sender: oneshot::Sender>, - }, - StorageSlot { - address: Address, - index: U256, - sender: oneshot::Sender>, - }, - Terminate, -} - -impl Request -where - E: Debug, -{ - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn handle(self, state: &mut S) -> bool - where - S: StateRef - + DatabaseCommit - + StateDebug - + StateHistory - + Debug, - { - match self { - Request::AccountByAddress { address, sender } => { - sender.send(state.basic(address)).unwrap() - } - Request::AccountStorageRoot { address, sender } => { - sender.send(state.account_storage_root(&address)).unwrap() - } - Request::Checkpoint { sender } => sender.send(state.checkpoint()).unwrap(), - Request::CodeByHash { code_hash, sender } => { - sender.send(state.code_by_hash(code_hash)).unwrap() - } - Request::Commit { changes, sender } => { - state.commit(changes); - sender.send(()).unwrap() - } - Request::InsertAccount { - address, - account_info, - sender, - } => sender - .send(state.insert_account(address, account_info)) - .unwrap(), - Request::MakeSnapshot { sender } => sender.send(state.make_snapshot()).unwrap(), - Request::ModifyAccount { - address, - modifier, - sender, - } => sender - .send(state.modify_account(address, modifier)) - .unwrap(), - Request::RemoveAccount { address, sender } => { - sender.send(state.remove_account(address)).unwrap() - } - Request::RemoveSnapshot { state_root, sender } => { - sender.send(state.remove_snapshot(&state_root)).unwrap() - } - Request::Revert { sender } => sender.send(state.revert()).unwrap(), - Request::Serialize { sender } => sender.send(state.serialize()).unwrap(), - Request::SetStorageSlot { - address, - index, - value, - sender, - } => sender - .send(state.set_account_storage_slot(address, index, value)) - .unwrap(), - Request::SetStateRoot { state_root, sender } => { - sender.send(state.set_state_root(&state_root)).unwrap() - } - Request::StateRoot { sender } => sender.send(state.state_root()).unwrap(), - Request::StorageSlot { - address, - index, - sender, - } => sender.send(state.storage(address, index)).unwrap(), - Request::Terminate => return false, - } - - true - } -} diff --git a/crates/rethnet_evm/src/state/sync.rs b/crates/rethnet_evm/src/state/sync.rs deleted file mode 100644 index ee1e00e383..0000000000 --- a/crates/rethnet_evm/src/state/sync.rs +++ /dev/null @@ -1,490 +0,0 @@ -use std::{fmt::Debug, io}; - -use hashbrown::HashMap; -use rethnet_eth::{Address, B256, U256}; -use revm::{ - db::StateRef, - primitives::{Account, AccountInfo, Bytecode}, - DatabaseCommit, -}; -use tokio::{ - runtime::{Builder, Runtime}, - sync::{ - mpsc::{unbounded_channel, UnboundedSender}, - oneshot, - }, - task::{self, JoinHandle}, -}; - -use crate::state::{AccountModifierFn, StateDebug}; - -use super::{history::StateHistory, request::Request}; - -/// Trait that meets all requirements for a synchronous database that can be used by [`AsyncDatabase`]. -pub trait SyncState: - StateRef - + DatabaseCommit - + StateDebug - + StateHistory - + Debug - + Send - + Sync - + 'static -where - E: Debug + Send, -{ -} - -impl SyncState for S -where - S: StateRef - + DatabaseCommit - + StateDebug - + StateHistory - + Debug - + Send - + Sync - + 'static, - E: Debug + Send, -{ -} - -/// A helper class for converting a synchronous database into an asynchronous database. -/// -/// Requires the inner database to implement [`Database`], [`DatabaseCommit`], and [`DatabaseDebug`]. - -#[derive(Debug)] -pub struct AsyncState -where - E: Debug + Send, -{ - runtime: Runtime, - request_sender: UnboundedSender>, - db_handle: Option>, -} - -impl AsyncState -where - E: Debug + Send + 'static, -{ - /// Constructs an [`AsyncDatabase`] instance with the provided database. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn new>(mut state: S) -> io::Result { - let runtime = Builder::new_multi_thread().build()?; - - let (sender, mut receiver) = unbounded_channel::>(); - - let db_handle = runtime.spawn(async move { - while let Some(request) = receiver.recv().await { - if !request.handle(&mut state) { - break; - } - } - }); - - Ok(Self { - runtime, - request_sender: sender, - db_handle: Some(db_handle), - }) - } - - /// Retrieves the runtime of the [`AsyncDatabase`]. - pub fn runtime(&self) -> &Runtime { - &self.runtime - } - - /// Retrieves the account corresponding to the specified address. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn account_by_address(&self, address: Address) -> Result, E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::AccountByAddress { address, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Retrieves the storage root of the account at the specified address. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn account_storage_root(&self, address: &Address) -> Result, E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::AccountStorageRoot { - address: *address, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Retrieves the storage slot corresponding to the specified address and index. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn account_storage_slot(&self, address: Address, index: U256) -> Result { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::StorageSlot { - address, - index, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Applies the provided changes to the state. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn apply(&self, changes: HashMap) { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::Commit { changes, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Creates a state checkpoint that can be reverted to using [`revert`]. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn checkpoint(&self) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::Checkpoint { sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Retrieves the code corresponding to the specified hash. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn code_by_hash(&self, code_hash: B256) -> Result { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::CodeByHash { code_hash, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Inserts the specified account into the state. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn insert_account( - &self, - address: Address, - account_info: AccountInfo, - ) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::InsertAccount { - address, - account_info, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Makes a snapshot of the database that's retained until [`remove_snapshot`] is called. Returns the snapshot's identifier. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn make_snapshot(&self) -> (B256, bool) { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::MakeSnapshot { sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Modifies the account at the specified address using the provided function. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn modify_account( - &self, - address: Address, - modifier: AccountModifierFn, - ) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::ModifyAccount { - address, - modifier, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Removes and returns the account at the specified address, if it exists. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn remove_account(&self, address: Address) -> Result, E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::RemoveAccount { address, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Removes the snapshot corresponding to the specified id, if it exists. Returns whether a snapshot was removed. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn remove_snapshot(&self, state_root: B256) -> bool { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::RemoveSnapshot { state_root, sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Reverts to the previous checkpoint, created using [`checkpoint`]. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn revert(&self) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::Revert { sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Serializes the state using ordering of addresses and storage indices. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn serialize(&self) -> String { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::Serialize { sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Sets the storage slot at the specified address and index to the provided value. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn set_account_storage_slot( - &self, - address: Address, - index: U256, - value: U256, - ) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::SetStorageSlot { - address, - index, - value, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Reverts the state to match the specified state root. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn set_state_root(&self, state_root: &B256) -> Result<(), E> { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::SetStateRoot { - state_root: *state_root, - sender, - }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } - - /// Retrieves the state's root. - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub async fn state_root(&self) -> Result { - let (sender, receiver) = oneshot::channel(); - - self.request_sender - .send(Request::StateRoot { sender }) - .expect("Failed to send request"); - - receiver.await.unwrap() - } -} - -impl Drop for AsyncState -where - E: Debug + Send, -{ - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn drop(&mut self) { - if let Some(handle) = self.db_handle.take() { - self.request_sender - .send(Request::Terminate) - .expect("Failed to send request"); - - self.runtime.block_on(handle).unwrap(); - } - } -} - -impl StateRef for AsyncState -where - E: Debug + Send + 'static, -{ - type Error = E; - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn basic(&self, address: Address) -> Result, Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::account_by_address(self, address)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn code_by_hash(&self, code_hash: B256) -> Result { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::code_by_hash(self, code_hash)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn storage(&self, address: Address, index: U256) -> Result { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::account_storage_slot(self, address, index)) - }) - } -} - -impl<'d, E> DatabaseCommit for &'d AsyncState -where - E: Debug + Send + 'static, -{ - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn commit(&mut self, changes: HashMap) { - task::block_in_place(move || self.runtime.block_on(self.apply(changes))) - } -} - -impl<'d, E> StateDebug for &'d AsyncState -where - E: Debug + Send + 'static, -{ - type Error = E; - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn account_storage_root(&mut self, address: &Address) -> Result, Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::account_storage_root(*self, address)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn insert_account( - &mut self, - address: Address, - account_info: AccountInfo, - ) -> Result<(), Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::insert_account(*self, address, account_info)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn modify_account( - &mut self, - address: Address, - modifier: AccountModifierFn, - ) -> Result<(), Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::modify_account(*self, address, modifier)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn remove_account(&mut self, address: Address) -> Result, Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::remove_account(*self, address)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn serialize(&mut self) -> String { - task::block_in_place(move || self.runtime.block_on(AsyncState::serialize(*self))) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn set_account_storage_slot( - &mut self, - address: Address, - index: U256, - value: U256, - ) -> Result<(), Self::Error> { - task::block_in_place(move || { - self.runtime.block_on(AsyncState::set_account_storage_slot( - *self, address, index, value, - )) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn state_root(&mut self) -> Result { - task::block_in_place(move || self.runtime.block_on(AsyncState::state_root(*self))) - } -} - -impl<'d, E> StateHistory for &'d AsyncState -where - E: Debug + Send + 'static, -{ - type Error = E; - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn set_state_root(&mut self, state_root: &B256) -> Result<(), Self::Error> { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::set_state_root(*self, state_root)) - }) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn checkpoint(&mut self) -> Result<(), Self::Error> { - task::block_in_place(move || self.runtime.block_on(AsyncState::checkpoint(*self))) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn revert(&mut self) -> Result<(), Self::Error> { - task::block_in_place(move || self.runtime.block_on(AsyncState::revert(*self))) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn make_snapshot(&mut self) -> (B256, bool) { - task::block_in_place(move || self.runtime.block_on(AsyncState::make_snapshot(*self))) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn remove_snapshot(&mut self, state_root: &B256) -> bool { - task::block_in_place(move || { - self.runtime - .block_on(AsyncState::remove_snapshot(*self, *state_root)) - }) - } -} diff --git a/crates/rethnet_evm/src/trace.rs b/crates/rethnet_evm/src/trace.rs index 106e53ba6c..ca5b8403f1 100644 --- a/crates/rethnet_evm/src/trace.rs +++ b/crates/rethnet_evm/src/trace.rs @@ -1,7 +1,7 @@ use rethnet_eth::Bytes; use revm::{ interpreter::{opcode, Gas, InstructionResult, Interpreter}, - Database, EVMData, Inspector, + EVMData, Inspector, }; /// A trace for an EVM call. @@ -63,14 +63,11 @@ impl TraceCollector { } } -impl Inspector for TraceCollector -where - D: Database, -{ +impl Inspector for TraceCollector { fn step( &mut self, interp: &mut Interpreter, - _data: &mut EVMData<'_, D>, + _data: &mut dyn EVMData, _is_static: bool, ) -> InstructionResult { self.opcode_stack.push(interp.current_opcode()); @@ -81,7 +78,7 @@ where fn step_end( &mut self, interp: &mut Interpreter, - _data: &mut EVMData<'_, D>, + _data: &mut dyn revm::EVMData, _is_static: bool, exit_code: InstructionResult, ) -> InstructionResult { diff --git a/crates/rethnet_evm_napi/src/blockchain.rs b/crates/rethnet_evm_napi/src/blockchain.rs index cec1916abc..0bf7c28806 100644 --- a/crates/rethnet_evm_napi/src/blockchain.rs +++ b/crates/rethnet_evm_napi/src/blockchain.rs @@ -2,13 +2,12 @@ mod js_blockchain; use std::{fmt::Debug, sync::Arc}; -use napi::{bindgen_prelude::Buffer, Env, JsFunction, NapiRaw, Status}; +use napi::{bindgen_prelude::Buffer, tokio::sync::RwLock, Env, JsFunction, NapiRaw}; use napi_derive::napi; use rethnet_eth::B256; -use rethnet_evm::blockchain::{AsyncBlockchain, SyncBlockchain}; +use rethnet_evm::blockchain::SyncBlockchain; use crate::{ - logger::enable_logging, sync::{await_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}, }; @@ -19,12 +18,12 @@ use self::js_blockchain::{GetBlockHashCall, JsBlockchain}; #[napi] #[derive(Debug)] pub struct Blockchain { - inner: Arc>, + inner: Arc>>>, } impl Blockchain { /// Provides immutable access to the inner implementation. - pub fn as_inner(&self) -> &Arc> { + pub fn as_inner(&self) -> &Arc>>> { &self.inner } } @@ -39,8 +38,6 @@ impl Blockchain { #[napi(ts_arg_type = "(blockNumber: bigint) => Promise")] get_block_hash_fn: JsFunction, ) -> napi::Result { - enable_logging(); - let get_block_hash_fn = ThreadsafeFunction::create( env.raw(), unsafe { get_block_hash_fn.raw() }, @@ -59,21 +56,19 @@ impl Blockchain { }, )?; - Self::with_blockchain(JsBlockchain { get_block_hash_fn }) + Ok(Self::with_blockchain(JsBlockchain { get_block_hash_fn })) } #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - fn with_blockchain(blockchain: B) -> napi::Result + fn with_blockchain(blockchain: B) -> Self where B: SyncBlockchain, { let blockchain: Box> = Box::new(blockchain); - let blockchain = AsyncBlockchain::new(blockchain) - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; - Ok(Self { - inner: Arc::new(blockchain), - }) + Self { + inner: Arc::new(RwLock::new(blockchain)), + } } // #[napi] diff --git a/crates/rethnet_evm_napi/src/blockchain/js_blockchain.rs b/crates/rethnet_evm_napi/src/blockchain/js_blockchain.rs index 44837a8abe..83b7b0e6fc 100644 --- a/crates/rethnet_evm_napi/src/blockchain/js_blockchain.rs +++ b/crates/rethnet_evm_napi/src/blockchain/js_blockchain.rs @@ -1,8 +1,11 @@ -use std::sync::mpsc::{channel, Sender}; +use std::{ + fmt::Debug, + sync::mpsc::{channel, Sender}, +}; use napi::Status; use rethnet_eth::{B256, U256}; -use rethnet_evm::BlockHash; +use rethnet_evm::BlockHashRef; use crate::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; @@ -15,10 +18,10 @@ pub struct JsBlockchain { pub(super) get_block_hash_fn: ThreadsafeFunction, } -impl BlockHash for JsBlockchain { +impl BlockHashRef for JsBlockchain { type Error = napi::Error; - fn block_hash(&mut self, block_number: U256) -> Result { + fn block_hash(&self, block_number: U256) -> Result { let (sender, receiver) = channel(); let status = self.get_block_hash_fn.call( @@ -33,3 +36,9 @@ impl BlockHash for JsBlockchain { receiver.recv().unwrap() } } + +impl Debug for JsBlockchain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsBlockchain").finish() + } +} diff --git a/crates/rethnet_evm_napi/src/context.rs b/crates/rethnet_evm_napi/src/context.rs new file mode 100644 index 0000000000..13073efcd5 --- /dev/null +++ b/crates/rethnet_evm_napi/src/context.rs @@ -0,0 +1,79 @@ +use std::{io, sync::Arc}; + +use napi::{ + tokio::runtime::{Builder, Runtime}, + Status, +}; +use napi_derive::napi; +use tracing_subscriber::{prelude::*, EnvFilter, Registry}; + +#[napi] +#[derive(Debug)] +pub struct RethnetContext { + inner: Arc, +} + +impl RethnetContext { + /// Provides immutable access to the inner implementation. + pub(crate) fn as_inner(&self) -> &Arc { + &self.inner + } +} + +#[napi] +impl RethnetContext { + /// Creates a new [`RethnetContext`] instance. Should only be called once! + #[napi(constructor)] + pub fn new() -> napi::Result { + let context = + Context::new().map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; + + Ok(Self { + inner: Arc::new(context), + }) + } +} + +#[derive(Debug)] +pub struct Context { + runtime: Arc, + #[cfg(feature = "tracing")] + _tracing_write_guard: tracing_flame::FlushGuard>, +} + +impl Context { + /// Creates a new [`Context`] instance. Should only be called once! + pub fn new() -> io::Result { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_target(false) + .with_level(true) + .with_filter(EnvFilter::from_default_env()); + + #[cfg(feature = "tracing")] + let (flame_layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded").unwrap(); + + let subscriber = Registry::default().with(fmt_layer); + + #[cfg(feature = "tracing")] + let subscriber = subscriber.with(flame_layer); + + tracing::subscriber::set_global_default(subscriber) + .expect("Could not set global default tracing subscriber"); + + let runtime = Builder::new_multi_thread().build()?; + + Ok(Self { + runtime: Arc::new(runtime), + #[cfg(feature = "tracing")] + _tracing_write_guard: guard, + }) + } + + /// Retrieves the context's runtime. + pub fn runtime(&self) -> &Runtime { + &self.runtime + } +} diff --git a/crates/rethnet_evm_napi/src/lib.rs b/crates/rethnet_evm_napi/src/lib.rs index be2f80bc41..789b837f24 100644 --- a/crates/rethnet_evm_napi/src/lib.rs +++ b/crates/rethnet_evm_napi/src/lib.rs @@ -7,8 +7,8 @@ mod block; mod blockchain; mod cast; mod config; +mod context; mod log; -mod logger; mod receipt; /// Rethnet runtime for executing individual transactions mod runtime; diff --git a/crates/rethnet_evm_napi/src/logger.rs b/crates/rethnet_evm_napi/src/logger.rs deleted file mode 100644 index af5da362c8..0000000000 --- a/crates/rethnet_evm_napi/src/logger.rs +++ /dev/null @@ -1,39 +0,0 @@ -use once_cell::sync::OnceCell; -use tracing_subscriber::{prelude::*, EnvFilter, Registry}; - -struct Logger { - #[cfg(feature = "tracing")] - _guard: tracing_flame::FlushGuard>, -} - -unsafe impl Sync for Logger {} - -static LOGGER: OnceCell = OnceCell::new(); - -pub fn enable_logging() { - let _logger = LOGGER.get_or_init(|| { - let fmt_layer = tracing_subscriber::fmt::layer() - .with_file(true) - .with_line_number(true) - .with_thread_ids(true) - .with_target(false) - .with_level(true) - .with_filter(EnvFilter::from_default_env()); - - #[cfg(feature = "tracing")] - let (flame_layer, _guard) = tracing_flame::FlameLayer::with_file("tracing.folded").unwrap(); - - let subscriber = Registry::default().with(fmt_layer); - - #[cfg(feature = "tracing")] - let subscriber = subscriber.with(flame_layer); - - tracing::subscriber::set_global_default(subscriber) - .expect("Could not set global default tracing subscriber"); - - Logger { - #[cfg(feature = "tracing")] - _guard, - } - }); -} diff --git a/crates/rethnet_evm_napi/src/state.rs b/crates/rethnet_evm_napi/src/state.rs index 4e601a3f0f..e5d458f397 100644 --- a/crates/rethnet_evm_napi/src/state.rs +++ b/crates/rethnet_evm_napi/src/state.rs @@ -8,12 +8,13 @@ use std::{ use napi::{ bindgen_prelude::{BigInt, Buffer, ObjectFinalize}, + tokio::sync::RwLock, Env, JsFunction, JsObject, NapiRaw, Status, }; use napi_derive::napi; use rethnet_eth::{signature::private_key_to_address, Address, Bytes, B256, U256}; use rethnet_evm::{ - state::{AccountModifierFn, AsyncState, HybridState, StateError, StateHistory, SyncState}, + state::{AccountModifierFn, HybridState, StateError, StateHistory, SyncState}, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY, }; use secp256k1::Secp256k1; @@ -21,7 +22,7 @@ use secp256k1::Secp256k1; use crate::{ account::Account, cast::TryCast, - logger::enable_logging, + context::{Context, RethnetContext}, sync::{await_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; @@ -59,7 +60,8 @@ pub struct SnapshotId { #[napi(custom_finalize)] #[derive(Debug)] pub struct StateManager { - pub(super) state: Arc>, + pub(super) state: Arc>>>, + context: Arc, } #[napi] @@ -67,8 +69,8 @@ impl StateManager { /// Constructs a [`StateManager`] with an empty state. #[napi(constructor)] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn new(mut env: Env) -> napi::Result { - Self::with_accounts(&mut env, HashMap::default()) + pub fn new(mut env: Env, context: &RethnetContext) -> napi::Result { + Self::with_accounts(&mut env, context, HashMap::default()) } /// Constructs a [`StateManager`] with the provided accounts present in the genesis state. @@ -76,13 +78,14 @@ impl StateManager { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn with_genesis_accounts( mut env: Env, + context: &RethnetContext, accounts: Vec, ) -> napi::Result { - let context = Secp256k1::signing_only(); + let signer = Secp256k1::signing_only(); let genesis_accounts = accounts .into_iter() .map(|account| { - let address = private_key_to_address(&context, &account.private_key) + let address = private_key_to_address(&signer, &account.private_key) .map_err(|e| napi::Error::new(Status::InvalidArg, e.to_string()))?; TryCast::::try_cast(account.balance).map(|balance| { let account_info = AccountInfo { @@ -95,12 +98,13 @@ impl StateManager { }) .collect::>>()?; - Self::with_accounts(&mut env, genesis_accounts) + Self::with_accounts(&mut env, context, genesis_accounts) } #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn with_accounts( env: &mut Env, + context: &RethnetContext, mut accounts: HashMap, ) -> napi::Result { // Mimic precompiles activation @@ -114,24 +118,21 @@ impl StateManager { state.checkpoint().unwrap(); - Self::with_state(env, state) + Self::with_state(env, context, state) } #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - fn with_state(env: &mut Env, state: S) -> napi::Result + fn with_state(env: &mut Env, context: &RethnetContext, state: S) -> napi::Result where S: SyncState, { - enable_logging(); - let state: Box> = Box::new(state); - let state = AsyncState::new(state) - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; env.adjust_external_memory(STATE_MEMORY_SIZE)?; Ok(Self { - state: Arc::new(state), + state: Arc::new(RwLock::new(state)), + context: context.as_inner().clone(), }) } @@ -139,21 +140,31 @@ impl StateManager { #[napi] #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn checkpoint(&self) -> napi::Result<()> { - self.state - .checkpoint() + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.checkpoint() + }) .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; - - Ok(()) + .unwrap() + .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } /// Reverts to the previous checkpoint, created using [`checkpoint`]. #[napi] #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn revert(&self) -> napi::Result<()> { - self.state - .revert() + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.revert() + }) .await + .unwrap() .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } @@ -163,24 +174,28 @@ impl StateManager { pub async fn get_account_by_address(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); - let mut account_info = self - .state - .account_by_address(address) - .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?; - - if let Some(account_info) = &mut account_info { - if account_info.code.is_none() && account_info.code_hash != KECCAK_EMPTY { - account_info.code = Some( - self.state - .code_by_hash(account_info.code_hash) - .await - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string()))?, - ); - } - } + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let state = state.read().await; + + state.basic(address).and_then(|account_info| { + account_info.map_or(Ok(None), |mut account_info| { + if account_info.code_hash != KECCAK_EMPTY { + account_info.code = Some(state.code_by_hash(account_info.code_hash)?); + } - Ok(account_info.map(Account::from)) + Ok(Some(account_info)) + }) + }) + }) + .await + .unwrap() + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |account_info| Ok(account_info.map(Account::from)), + ) } /// Retrieves the storage root of the account at the specified address. @@ -189,10 +204,19 @@ impl StateManager { pub async fn get_account_storage_root(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); - self.state.account_storage_root(&address).await.map_or_else( - |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), - |root| Ok(root.map(|root| Buffer::from(root.as_ref()))), - ) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.account_storage_root(&address) + }) + .await + .unwrap() + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |root| Ok(root.map(|root| Buffer::from(root.as_ref()))), + ) } /// Retrieves the storage slot at the specified address and index. @@ -206,9 +230,15 @@ impl StateManager { let address = Address::from_slice(&address); let index: U256 = BigInt::try_cast(index)?; - self.state - .account_storage_slot(address, index) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let state = state.read().await; + state.storage(address, index) + }) .await + .unwrap() .map_or_else( |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), |value| { @@ -224,10 +254,19 @@ impl StateManager { #[napi] #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get_state_root(&self) -> napi::Result { - self.state.state_root().await.map_or_else( - |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), - |root| Ok(Buffer::from(root.as_ref())), - ) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.state_root() + }) + .await + .unwrap() + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |root| Ok(Buffer::from(root.as_ref())), + ) } /// Inserts the provided account at the specified address. @@ -235,11 +274,17 @@ impl StateManager { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn insert_account(&self, address: Buffer, account: Account) -> napi::Result<()> { let address = Address::from_slice(&address); - let account: AccountInfo = account.try_cast()?; - - self.state - .insert_account(address, account) + let account_info: AccountInfo = account.try_cast()?; + + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.insert_account(address, account_info) + }) .await + .unwrap() .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } @@ -247,7 +292,16 @@ impl StateManager { #[napi] #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn make_snapshot(&self) -> SnapshotId { - let (state_root, existed) = self.state.make_snapshot().await; + let state = self.state.clone(); + let (state_root, existed) = self + .context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.make_snapshot() + }) + .await + .unwrap(); SnapshotId { state_root: >::as_ref(&state_root).into(), @@ -330,10 +384,11 @@ impl StateManager { )?; let (deferred, promise) = env.create_deferred()?; - let db = self.state.clone(); + let state = self.state.clone(); + self.context.runtime().spawn(async move { + let mut state = state.write().await; - self.state.runtime().spawn(async move { - let result = db + let result = state .modify_account( address, AccountModifierFn::new(Box::new( @@ -359,7 +414,6 @@ impl StateManager { }, )), ) - .await .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())); deferred.resolve(|_| result); @@ -374,10 +428,19 @@ impl StateManager { pub async fn remove_account(&self, address: Buffer) -> napi::Result> { let address = Address::from_slice(&address); - self.state.remove_account(address).await.map_or_else( - |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), - |account| Ok(account.map(Account::from)), - ) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.remove_account(address) + }) + .await + .unwrap() + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |account| Ok(account.map(Account::from)), + ) } /// Removes the snapshot corresponding to the specified state root, if it exists. Returns whether a snapshot was removed. @@ -386,14 +449,30 @@ impl StateManager { pub async fn remove_snapshot(&self, state_root: Buffer) -> bool { let state_root = B256::from_slice(&state_root); - self.state.remove_snapshot(state_root).await + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.remove_snapshot(&state_root) + }) + .await + .unwrap() } /// Serializes the state using ordering of addresses and storage indices. #[napi] #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn serialize(&self) -> String { - self.state.serialize().await + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.serialize() + }) + .await + .unwrap() } /// Sets the storage slot at the specified address and index to the provided value. @@ -409,9 +488,15 @@ impl StateManager { let index: U256 = BigInt::try_cast(index)?; let value: U256 = BigInt::try_cast(value)?; - self.state - .set_account_storage_slot(address, index, value) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.set_account_storage_slot(address, index, value) + }) .await + .unwrap() .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } @@ -421,9 +506,15 @@ impl StateManager { pub async fn set_state_root(&self, state_root: Buffer) -> napi::Result<()> { let state_root = B256::from_slice(&state_root); - self.state - .set_state_root(&state_root) + let state = self.state.clone(); + self.context + .runtime() + .spawn(async move { + let mut state = state.write().await; + state.set_state_root(&state_root) + }) .await + .unwrap() .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) } } diff --git a/crates/rethnet_evm_napi/src/tracer.rs b/crates/rethnet_evm_napi/src/tracer.rs index 1de31c2d8a..f7020949f2 100644 --- a/crates/rethnet_evm_napi/src/tracer.rs +++ b/crates/rethnet_evm_napi/src/tracer.rs @@ -2,7 +2,7 @@ mod js_tracer; use napi::Env; use napi_derive::napi; -use rethnet_evm::{state::StateError, AsyncInspector}; +use rethnet_evm::{state::StateError, SyncInspector}; use self::js_tracer::{JsTracer, TracingCallbacks}; @@ -12,7 +12,7 @@ pub struct Tracer { } impl Tracer { - pub fn as_dyn_inspector(&self) -> Box> { + pub fn as_dyn_inspector(&self) -> Box> { self.inner.clone() } } diff --git a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs index 21a3459b21..75974f6356 100644 --- a/crates/rethnet_evm_napi/src/tracer/js_tracer.rs +++ b/crates/rethnet_evm_napi/src/tracer/js_tracer.rs @@ -11,7 +11,7 @@ use napi::{ use napi_derive::napi; use rethnet_eth::{Address, Bytes, U256}; use rethnet_evm::{ - opcode, return_revert, AsyncInspector, Bytecode, Gas, InstructionResult, SuccessOrHalt, + opcode, return_revert, Bytecode, Gas, InstructionResult, SuccessOrHalt, SyncInspector, }; use crate::{ @@ -602,50 +602,54 @@ impl Debug for JsTracer { } } -impl AsyncInspector for JsTracer +impl SyncInspector for JsTracer where BE: Debug + Send + 'static, SE: Debug + Send + 'static, { } -impl rethnet_evm::Inspector for JsTracer +impl rethnet_evm::Inspector for JsTracer where - D: rethnet_evm::Database, - D::Error: Debug, + E: Debug, { fn call( &mut self, - data: &mut rethnet_evm::EVMData<'_, D>, + data: &mut dyn rethnet_evm::EVMData, inputs: &mut rethnet_evm::CallInputs, _is_static: bool, ) -> (InstructionResult, Gas, rethnet_eth::Bytes) { self.validate_before_message(); let code = data - .journaled_state + .journaled_state() .state .get(&inputs.context.code_address) + .cloned() .map(|account| { if let Some(code) = &account.info.code { code.clone() } else { - data.db.code_by_hash(account.info.code_hash).unwrap() + data.database() + .code_by_hash(account.info.code_hash) + .unwrap() } }) .unwrap_or_else(|| { - data.db.basic(inputs.context.code_address).unwrap().map_or( - Bytecode::new(), - |account_info| { + data.database() + .basic(inputs.context.code_address) + .unwrap() + .map_or(Bytecode::new(), |account_info| { account_info.code.unwrap_or_else(|| { - data.db.code_by_hash(account_info.code_hash).unwrap() + data.database() + .code_by_hash(account_info.code_hash) + .unwrap() }) - }, - ) + }) }); self.pending_before = Some(BeforeMessage { - depth: data.journaled_state.depth, + depth: data.journaled_state().depth, to: Some(inputs.context.address), data: inputs.input.clone(), value: inputs.context.apparent_value, @@ -658,7 +662,7 @@ where fn call_end( &mut self, - data: &mut rethnet_evm::EVMData<'_, D>, + data: &mut dyn rethnet_evm::EVMData, _inputs: &rethnet_evm::CallInputs, remaining_gas: Gas, ret: InstructionResult, @@ -689,7 +693,7 @@ where reason, gas_used: remaining_gas.spend(), gas_refunded: remaining_gas.refunded() as u64, - logs: data.journaled_state.logs.clone(), + logs: data.journaled_state().logs.clone(), output: rethnet_evm::Output::Call(out.clone()), }, SuccessOrHalt::Revert => rethnet_evm::ExecutionResult::Revert { @@ -700,7 +704,7 @@ where reason, gas_used: remaining_gas.limit(), }, - SuccessOrHalt::Internal => panic!("Internal error: {:?}", safe_ret), + SuccessOrHalt::InternalContinue => panic!("Internal error: {:?}", safe_ret), SuccessOrHalt::FatalExternalError => panic!("Fatal external error"), }; @@ -722,13 +726,13 @@ where fn create( &mut self, - data: &mut rethnet_evm::EVMData<'_, D>, + data: &mut dyn rethnet_evm::EVMData, inputs: &mut rethnet_evm::CreateInputs, ) -> (InstructionResult, Option, Gas, Bytes) { self.validate_before_message(); self.pending_before = Some(BeforeMessage { - depth: data.journaled_state.depth, + depth: data.journaled_state().depth, to: None, data: inputs.init_code.clone(), value: inputs.value, @@ -746,7 +750,7 @@ where fn create_end( &mut self, - data: &mut rethnet_evm::EVMData<'_, D>, + data: &mut dyn rethnet_evm::EVMData, _inputs: &rethnet_evm::CreateInputs, ret: InstructionResult, address: Option, @@ -767,7 +771,7 @@ where reason, gas_used: remaining_gas.spend(), gas_refunded: remaining_gas.refunded() as u64, - logs: data.journaled_state.logs.clone(), + logs: data.journaled_state().logs.clone(), output: rethnet_evm::Output::Create(out.clone(), address), }, SuccessOrHalt::Revert => rethnet_evm::ExecutionResult::Revert { @@ -778,7 +782,7 @@ where reason, gas_used: remaining_gas.limit(), }, - SuccessOrHalt::Internal => panic!("Internal error: {:?}", safe_ret), + SuccessOrHalt::InternalContinue => panic!("Internal error: {:?}", safe_ret), SuccessOrHalt::FatalExternalError => panic!("Fatal external error"), }; @@ -801,7 +805,7 @@ where fn step( &mut self, interp: &mut rethnet_evm::Interpreter, - data: &mut rethnet_evm::EVMData<'_, D>, + data: &mut dyn rethnet_evm::EVMData, _is_static: bool, ) -> InstructionResult { // Skip the step @@ -823,7 +827,7 @@ where let status = self.step_fn.call( StepHandlerCall { - depth: data.journaled_state.depth, + depth: data.journaled_state().depth, pc: interp.program_counter() as u64, opcode: interp.current_opcode(), // return_value: interp.instruction_result, @@ -833,7 +837,7 @@ where // stack: interp.stack().data().clone(), // memory: Bytes::copy_from_slice(interp.memory.data().as_slice()), contract: data - .journaled_state + .journaled_state() .account(interp.contract.address) .info .clone(), @@ -856,7 +860,7 @@ where // fn step_end( // &mut self, // interp: &mut rethnet_evm::Interpreter, - // _data: &mut rethnet_evm::EVMData<'_, D>, + // _data: &mut dyn rethnet_evm::EVMData, // _is_static: bool, // _eval: InstructionResult, // ) -> InstructionResult { diff --git a/crates/rethnet_evm_napi/src/transaction/result.rs b/crates/rethnet_evm_napi/src/transaction/result.rs index b11042cbbb..4e16d918bc 100644 --- a/crates/rethnet_evm_napi/src/transaction/result.rs +++ b/crates/rethnet_evm_napi/src/transaction/result.rs @@ -78,13 +78,14 @@ pub enum ExceptionalHalt { StackOverflow, OutOfOffset, CreateCollision, - OverflowPayment, PrecompileError, NonceOverflow, /// Create init code size exceeds limit (runtime). CreateContractSizeLimit, /// Error on created contract that begins with EF CreateContractStartingWithEF, + /// EIP-3860: Limit and meter initcode. Initcode size limit exceeded. + CreateInitcodeSizeLimit, } impl From for ExceptionalHalt { @@ -99,13 +100,20 @@ impl From for ExceptionalHalt { rethnet_evm::Halt::StackOverflow => ExceptionalHalt::StackOverflow, rethnet_evm::Halt::OutOfOffset => ExceptionalHalt::OutOfOffset, rethnet_evm::Halt::CreateCollision => ExceptionalHalt::CreateCollision, - rethnet_evm::Halt::OverflowPayment => ExceptionalHalt::OverflowPayment, rethnet_evm::Halt::PrecompileError => ExceptionalHalt::PrecompileError, rethnet_evm::Halt::NonceOverflow => ExceptionalHalt::NonceOverflow, rethnet_evm::Halt::CreateContractSizeLimit => ExceptionalHalt::CreateContractSizeLimit, rethnet_evm::Halt::CreateContractStartingWithEF => { ExceptionalHalt::CreateContractStartingWithEF } + rethnet_evm::Halt::CreateInitcodeSizeLimit => ExceptionalHalt::CreateInitcodeSizeLimit, + rethnet_evm::Halt::OverflowPayment + | rethnet_evm::Halt::StateChangeDuringStaticCall + | rethnet_evm::Halt::CallNotAllowedInsideStatic + | rethnet_evm::Halt::OutOfFund + | rethnet_evm::Halt::CallTooDeep => { + unreachable!("Internal halts that can be only found inside Inspector") + } } } } diff --git a/crates/rethnet_evm_napi/test/evm/RethnetDb.ts b/crates/rethnet_evm_napi/test/evm/RethnetDb.ts index 993a0d81b4..78e71c79d2 100644 --- a/crates/rethnet_evm_napi/test/evm/RethnetDb.ts +++ b/crates/rethnet_evm_napi/test/evm/RethnetDb.ts @@ -1,96 +1,99 @@ -import { expect } from "chai"; -import { Address } from "@nomicfoundation/ethereumjs-util"; - -import { - Blockchain, - BlockConfig, - Config, - Rethnet, - SpecId, - StateManager, - Transaction, -} from "../.."; - -describe("Rethnet", () => { - const caller = Address.fromString( - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - ); - const receiver = Address.fromString( - "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" - ); - - let blockchain: Blockchain; - let stateManager: StateManager; - let rethnet: Rethnet; - - beforeEach(async function () { - blockchain = new Blockchain(async function ( - _blockNumber: bigint - ): Promise { - return Buffer.allocUnsafe(0); - }); - - stateManager = new StateManager(); - - const cfg: Config = { - chainId: BigInt(0), - specId: SpecId.GrayGlacier, - limitContractCodeSize: BigInt(2n) ** BigInt(32n), - disableEip3607: true, - }; - rethnet = new Rethnet(blockchain, stateManager, cfg); - }); - - it("call", async () => { - // Add funds to caller - await stateManager.insertAccount(caller.buf, { - nonce: 0n, - balance: BigInt("0xffffffff"), - }); - - // send some value - const sendValue: Transaction = { - from: caller.buf, - to: receiver.buf, - gasLimit: BigInt(1000000), - value: 100n, - }; - - const block: BlockConfig = { - number: BigInt(1), - timestamp: BigInt(Math.ceil(new Date().getTime() / 1000)), - }; - let sendValueChanges = await rethnet.dryRun(sendValue, block); - - // receiver should have 100 (0x64) wei - expect( - BigInt( - sendValueChanges.state["0x70997970c51812dc3a010c7d01b50e0d17dc79c8"] - .info.balance - ) - ).to.equal(BigInt("0x64")); - - // create a contract - const createContract: Transaction = { - from: caller.buf, - - gasLimit: BigInt(1000000), - - // minimal creation bytecode - input: Buffer.from("3859818153F3", "hex"), - }; - - let createContractChanges = await rethnet.dryRun(createContract, block); - - expect( - createContractChanges.state["0x5fbdb2315678afecb367f032d93f642f64180aa3"] - ).to.exist; - // check that the code hash is not the null hash (i.e., the address has code) - expect( - createContractChanges.state["0x5fbdb2315678afecb367f032d93f642f64180aa3"] - .info.code_hash - ).to.not.equal( - "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" - ); - }); -}); +// import { expect } from "chai"; +// import { Address } from "@nomicfoundation/ethereumjs-util"; + +// import { +// Blockchain, +// BlockConfig, +// Config, +// Rethnet, +// RethnetContext, +// SpecId, +// StateManager, +// Transaction, +// } from "../.."; + +// describe("Rethnet", () => { +// const caller = Address.fromString( +// "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +// ); +// const receiver = Address.fromString( +// "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +// ); + +// const context = new RethnetContext(); + +// let blockchain: Blockchain; +// let stateManager: StateManager; +// let rethnet: Rethnet; + +// beforeEach(async function () { +// blockchain = new Blockchain(async function ( +// _blockNumber: bigint +// ): Promise { +// return Buffer.allocUnsafe(0); +// }); + +// stateManager = new StateManager(context); + +// const cfg: Config = { +// chainId: BigInt(0), +// specId: SpecId.GrayGlacier, +// limitContractCodeSize: BigInt(2n) ** BigInt(32n), +// disableEip3607: true, +// }; +// rethnet = new Rethnet(blockchain, stateManager, cfg); +// }); + +// it("call", async () => { +// // Add funds to caller +// await stateManager.insertAccount(caller.buf, { +// nonce: 0n, +// balance: BigInt("0xffffffff"), +// }); + +// // send some value +// const sendValue: Transaction = { +// from: caller.buf, +// to: receiver.buf, +// gasLimit: BigInt(1000000), +// value: 100n, +// }; + +// const block: BlockConfig = { +// number: BigInt(1), +// timestamp: BigInt(Math.ceil(new Date().getTime() / 1000)), +// }; +// let sendValueChanges = await rethnet.dryRun(sendValue, block); + +// // receiver should have 100 (0x64) wei +// expect( +// BigInt( +// sendValueChanges .state["0x70997970c51812dc3a010c7d01b50e0d17dc79c8"] +// .info.balance +// ) +// ).to.equal(BigInt("0x64")); + +// // create a contract +// const createContract: Transaction = { +// from: caller.buf, + +// gasLimit: BigInt(1000000), + +// // minimal creation bytecode +// input: Buffer.from("3859818153F3", "hex"), +// }; + +// let createContractChanges = await rethnet.dryRun(createContract, block); + +// expect( +// createContractChanges.state["0x5fbdb2315678afecb367f032d93f642f64180aa3"] +// ).to.exist; +// // check that the code hash is not the null hash (i.e., the address has code) +// expect( +// createContractChanges.state["0x5fbdb2315678afecb367f032d93f642f64180aa3"] +// .info.code_hash +// ).to.not.equal( +// "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" +// ); +// }); +// }); diff --git a/crates/rethnet_evm_napi/test/evm/StateManager.ts b/crates/rethnet_evm_napi/test/evm/StateManager.ts index fc3be4e425..e65f9e9fa0 100644 --- a/crates/rethnet_evm_napi/test/evm/StateManager.ts +++ b/crates/rethnet_evm_napi/test/evm/StateManager.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { Address, KECCAK256_NULL } from "@nomicfoundation/ethereumjs-util"; -import { Account, Bytecode, StateManager } from "../.."; +import { Account, Bytecode, RethnetContext, StateManager } from "../.."; describe("State Manager", () => { const caller = Address.fromString( @@ -11,10 +11,12 @@ describe("State Manager", () => { "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" ); + const context = new RethnetContext(); + let stateManager: StateManager; beforeEach(function () { - stateManager = new StateManager(); + stateManager = new StateManager(context); }); // TODO: insertBlock, setAccountCode, setAccountStorageSlot diff --git a/packages/hardhat-core/src/internal/core/providers/http.ts b/packages/hardhat-core/src/internal/core/providers/http.ts index fba1c4e27d..56cb231027 100644 --- a/packages/hardhat-core/src/internal/core/providers/http.ts +++ b/packages/hardhat-core/src/internal/core/providers/http.ts @@ -264,7 +264,7 @@ export class HttpProvider extends EventEmitter implements EIP1193Provider { ): number | undefined { const header = response.headers["retry-after"]; - if (header === undefined || header === null) { + if (header === undefined || header === null || Array.isArray(header)) { return undefined; } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts index 1d6eff5329..0392d1ac14 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/RethnetState.ts @@ -3,20 +3,22 @@ import { bufferToBigInt, toBuffer, } from "@nomicfoundation/ethereumjs-util"; -import { StateManager, Account, Bytecode } from "rethnet-evm"; +import { StateManager, Account, Bytecode, RethnetContext } from "rethnet-evm"; import { GenesisAccount } from "./node-types"; /* eslint-disable @nomiclabs/hardhat-internal-rules/only-hardhat-error */ /* eslint-disable @typescript-eslint/no-unused-vars */ export class RethnetStateManager { - constructor(private _state: StateManager = new StateManager()) {} + constructor(private _state: StateManager) {} public static withGenesisAccounts( + context: RethnetContext, genesisAccounts: GenesisAccount[] ): RethnetStateManager { return new RethnetStateManager( StateManager.withGenesisAccounts( + context, genesisAccounts.map((account) => { return { privateKey: account.privateKey, diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts index 2d5bda6907..c383fcc8f9 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/rethnet.ts @@ -16,6 +16,7 @@ import { TracingMessage, TracingMessageResult, TracingStep, + RethnetContext, } from "rethnet-evm"; import { isForkedNodeConfig, NodeConfig } from "../node-types"; @@ -38,6 +39,8 @@ import { RunTxResult, Trace, VMAdapter } from "./vm-adapter"; /* eslint-disable @nomiclabs/hardhat-internal-rules/only-hardhat-error */ /* eslint-disable @typescript-eslint/no-unused-vars */ +const globalContext = new RethnetContext(); + export class RethnetAdapter implements VMAdapter { private _vmTracer: VMTracer; @@ -68,6 +71,7 @@ export class RethnetAdapter implements VMAdapter { config.allowUnlimitedContractSize === true ? 2n ** 64n - 1n : undefined; const state = RethnetStateManager.withGenesisAccounts( + globalContext, config.genesisAccounts );