diff --git a/packages/api-evm/source/actions/eth-estimate-gas.ts b/packages/api-evm/source/actions/eth-estimate-gas.ts index d59d41ae93..5595dd6af9 100644 --- a/packages/api-evm/source/actions/eth-estimate-gas.ts +++ b/packages/api-evm/source/actions/eth-estimate-gas.ts @@ -1,5 +1,3 @@ -import crypto from "node:crypto"; - import { inject, injectable, tagged } from "@mainsail/container"; import { Contracts, Exceptions, Identifiers } from "@mainsail/contracts"; import { assert } from "@mainsail/utils"; @@ -23,7 +21,7 @@ interface EstimateOutcome { @injectable() export class EthEstimateGasAction implements Contracts.Api.RPC.Action { @inject(Identifiers.Evm.Instance) - @tagged("instance", "validator") + @tagged("instance", "rpc") private readonly evm!: Contracts.Evm.Instance; @inject(Identifiers.Cryptography.Configuration) @@ -93,7 +91,6 @@ export class EthEstimateGasAction implements Contracts.Api.RPC.Action { nonce: accountInfo.nonce, specId: evmSpec, to: data.to, - txHash: this.#generateTxHash(), value: data.value ? BigInt(data.value) : BigInt(0), }; @@ -165,21 +162,12 @@ export class EthEstimateGasAction implements Contracts.Api.RPC.Action { return `0x${maxGasLimit.toString(16)}`; } - async #execute(context: Contracts.Evm.TransactionContext): Promise { - await this.evm.prepareNextCommit({ - commitKey: context.blockContext.commitKey, - }); - + async #execute(context: Contracts.Evm.TransactionSimulateContext): Promise { try { - const { receipt } = await this.evm.process(context); + const { receipt } = await this.evm.simulate(context); return { receipt, success: receipt.status === 1 }; } catch (error) { return { executionError: error.message, success: false }; } } - - #generateTxHash = () => { - const randomBytes = crypto.randomBytes(32); - return crypto.createHash("sha256").update(randomBytes).digest("hex"); - }; } diff --git a/packages/contracts/source/contracts/evm/evm.ts b/packages/contracts/source/contracts/evm/evm.ts index 4355532616..5b8cc9021a 100644 --- a/packages/contracts/source/contracts/evm/evm.ts +++ b/packages/contracts/source/contracts/evm/evm.ts @@ -9,7 +9,10 @@ export interface GenesisInfo { export interface ProcessResult { readonly receipt: TransactionReceipt; - readonly mocked?: boolean; +} + +export interface SimulateResult { + readonly receipt: TransactionReceipt; } export interface ViewResult { @@ -117,6 +120,19 @@ export interface TransactionContext { readonly specId: SpecId; } +export interface TransactionSimulateContext { + readonly from: string; + /** Omit recipient when deploying a contract */ + readonly to?: string; + readonly gasLimit: bigint; + readonly value: bigint; + readonly gasPrice: bigint; + readonly nonce: bigint; + readonly data: Buffer; + readonly blockContext: BlockContext; + readonly specId: SpecId; +} + export interface TransactionViewContext { readonly from: string; readonly to: string; diff --git a/packages/contracts/source/contracts/evm/instance.ts b/packages/contracts/source/contracts/evm/instance.ts index d84b8f0962..d0be619bc9 100644 --- a/packages/contracts/source/contracts/evm/instance.ts +++ b/packages/contracts/source/contracts/evm/instance.ts @@ -15,7 +15,9 @@ import { PreverifyTransactionContext, PreverifyTransactionResult, ProcessResult, + SimulateResult, TransactionContext, + TransactionSimulateContext, TransactionViewContext, UpdateRewardsAndVotesContext, ViewResult, @@ -25,6 +27,7 @@ export interface Instance extends CommitHandler { prepareNextCommit(context: PrepareNextCommitContext): Promise; preverifyTransaction(txContext: PreverifyTransactionContext): Promise; process(txContext: TransactionContext): Promise; + simulate(txContext: TransactionSimulateContext): Promise; view(viewContext: TransactionViewContext): Promise; initializeGenesis(commit: GenesisInfo): Promise; getAccountInfo(address: string, height?: bigint): Promise; diff --git a/packages/evm-service/source/instances/evm.ts b/packages/evm-service/source/instances/evm.ts index 6e5d9eae3e..1be8cd73cd 100644 --- a/packages/evm-service/source/instances/evm.ts +++ b/packages/evm-service/source/instances/evm.ts @@ -86,6 +86,10 @@ export class EvmInstance implements Contracts.Evm.Instance, Contracts.Evm.Storag return this.#evm.process(txContext); } + public async simulate(txContext: Contracts.Evm.TransactionSimulateContext): Promise { + return this.#evm.simulate(txContext); + } + public async initializeGenesis(info: Contracts.Evm.GenesisInfo): Promise { return this.#evm.initializeGenesis({ account: info.account, diff --git a/packages/evm/Cargo.toml b/packages/evm/Cargo.toml index 31560d3826..e35629aa18 100644 --- a/packages/evm/Cargo.toml +++ b/packages/evm/Cargo.toml @@ -4,7 +4,7 @@ members = ["core", "bindings"] [workspace.package] version = "0.1.0" -rust-version = "1.85" +rust-version = "1.88" edition = "2024" license = "GPL-3.0-only" authors = [""] diff --git a/packages/evm/bindings/src/ctx.rs b/packages/evm/bindings/src/ctx.rs index b1eca10f85..fa034170d6 100644 --- a/packages/evm/bindings/src/ctx.rs +++ b/packages/evm/bindings/src/ctx.rs @@ -34,6 +34,20 @@ pub struct JsTransactionContext { pub spec_id: JsString, } +#[napi(object)] +pub struct JsTransactionSimulateContext { + pub from: JsString, + /// Omit recipient when deploying a contract + pub to: Option, + pub gas_limit: JsBigInt, + pub gas_price: JsBigInt, + pub value: JsBigInt, + pub nonce: JsBigInt, + pub data: JsBuffer, + pub block_context: JsBlockContext, + pub spec_id: JsString, +} + #[napi(object)] pub struct JsPreverifyTransactionContext { pub from: JsString, @@ -163,6 +177,19 @@ pub struct TxViewContext { pub gas_limit: Option, } +#[derive(Debug)] +pub struct TxSimulateContext { + pub from: Address, + pub to: Option
, + pub gas_limit: u64, + pub gas_price: u128, + pub value: U256, + pub nonce: u64, + pub data: Bytes, + pub block_context: BlockContext, + pub spec_id: SpecId, +} + #[derive(Debug)] pub struct BlockContext { pub commit_key: CommitKey, @@ -217,6 +244,7 @@ pub struct ExecutionContext { pub tx_hash: Option, pub block_context: Option, pub spec_id: SpecId, + pub stateful: bool, } impl From for ExecutionContext { @@ -232,6 +260,7 @@ impl From for ExecutionContext { tx_hash: None, block_context: None, spec_id: value.spec_id, + stateful: false, } } } @@ -249,6 +278,25 @@ impl From for ExecutionContext { tx_hash: Some(value.tx_hash), block_context: Some(value.block_context), spec_id: value.spec_id, + stateful: true, + } + } +} + +impl From for ExecutionContext { + fn from(value: TxSimulateContext) -> Self { + Self { + from: value.from, + to: value.to, + gas_limit: Some(value.gas_limit), + gas_price: value.gas_price, + value: value.value, + nonce: Some(value.nonce), + data: value.data, + tx_hash: None, + block_context: Some(value.block_context), + spec_id: value.spec_id, + stateful: false, } } } @@ -367,6 +415,32 @@ impl TryFrom for TxContext { } } +impl TryFrom for TxSimulateContext { + type Error = anyhow::Error; + + fn try_from(mut value: JsTransactionSimulateContext) -> std::result::Result { + let buf = value.data.into_value()?; + + let to = if let Some(to) = value.to { + Some(utils::create_address_from_js_string(to)?) + } else { + None + }; + + Ok(TxSimulateContext { + to, + gas_limit: value.gas_limit.try_into()?, + gas_price: value.gas_price.get_u128()?.1, + from: utils::create_address_from_js_string(value.from)?, + value: utils::convert_bigint_to_u256(value.value)?, + nonce: value.nonce.get_u64()?.0, + data: Bytes::from(buf.as_ref().to_owned()), + block_context: value.block_context.try_into()?, + spec_id: parse_spec_id(value.spec_id)?, + }) + } +} + impl TryFrom for PreverifyTxContext { type Error = anyhow::Error; diff --git a/packages/evm/bindings/src/lib.rs b/packages/evm/bindings/src/lib.rs index 61e1df81a5..97360967a9 100644 --- a/packages/evm/bindings/src/lib.rs +++ b/packages/evm/bindings/src/lib.rs @@ -4,8 +4,9 @@ use ctx::{ BlockContext, CalculateRoundValidatorsContext, EvmOptions, ExecutionContext, GenesisContext, JsCalculateRoundValidatorsContext, JsCommitData, JsCommitKey, JsEvmOptions, JsGenesisContext, JsPrepareNextCommitContext, JsPreverifyTransactionContext, JsTransactionContext, - JsTransactionViewContext, JsUpdateRewardsAndVotesContext, PrepareNextCommitContext, - PreverifyTxContext, TxContext, TxViewContext, UpdateRewardsAndVotesContext, + JsTransactionSimulateContext, JsTransactionViewContext, JsUpdateRewardsAndVotesContext, + PrepareNextCommitContext, PreverifyTxContext, TxContext, TxSimulateContext, TxViewContext, + UpdateRewardsAndVotesContext, }; use logger::JsLogger; use mainsail_evm_core::{ @@ -254,6 +255,7 @@ impl EvmInner { gas_price: 0, spec_id: ctx.spec_id, tx_hash: None, + stateful: true, }) { Ok(receipt) => { self.logger.log( @@ -339,6 +341,7 @@ impl EvmInner { gas_price: 0, spec_id: ctx.spec_id, tx_hash: None, + stateful: true, }) { Ok(receipt) => { self.logger.log( @@ -662,6 +665,13 @@ impl EvmInner { } } + pub fn simulate( + &mut self, + ctx: TxSimulateContext, + ) -> std::result::Result> { + self.execute(ctx.into()) + } + pub fn process( &mut self, tx_ctx: TxContext, @@ -721,27 +731,7 @@ impl EvmInner { } } - let result = self.transact_evm(tx_ctx.into()); - - match result { - Ok(result) => { - let receipt = map_execution_result(result); - Ok(receipt) - } - Err(err) => { - match err { - EVMError::Transaction(err) => { - return Err(EVMError::Transaction(err)); - } - // EVMError::Header(_) => todo!(), - // EVMError::Database(_) => todo!(), - // EVMError::Custom(_) => todo!(), - _ => { - unimplemented!("fatal evm err {:?}", err); - } - } - } - } + self.execute(tx_ctx.into()) } pub fn commit( @@ -947,13 +937,40 @@ impl EvmInner { Ok(()) } + fn execute( + &mut self, + ctx: ExecutionContext, + ) -> std::result::Result> { + match self.transact_evm(ctx.into()) { + Ok(result) => { + let receipt = map_execution_result(result); + Ok(receipt) + } + Err(err) => { + match err { + EVMError::Transaction(err) => { + return Err(EVMError::Transaction(err)); + } + // EVMError::Header(_) => todo!(), + // EVMError::Database(_) => todo!(), + // EVMError::Custom(_) => todo!(), + _ => { + panic!("fatal evm err {:?}", err); + } + } + } + } + } + fn transact_evm( &mut self, ctx: ExecutionContext, ) -> std::result::Result> { let mut state_builder = State::builder().with_bundle_update(); - if let Some(commit_key) = ctx.block_context.as_ref().map(|b| &b.commit_key) { + if let Some(commit_key) = ctx.block_context.as_ref().map(|b| &b.commit_key) + && ctx.stateful + { if let Some(pending_commit) = self.pending_commits.get_mut(commit_key) { state_builder = state_builder.with_cached_prestate(std::mem::take(&mut pending_commit.cache)); @@ -1004,7 +1021,9 @@ impl EvmInner { let ResultAndState { state, result } = result; // Update state if transaction is part of a commit - if let Some(commit_key) = ctx.block_context.as_ref().map(|b| &b.commit_key) { + if let Some(commit_key) = ctx.block_context.as_ref().map(|b| &b.commit_key) + && ctx.stateful + { let state_db = evm.db_mut(); state_db.commit(state); @@ -1140,6 +1159,19 @@ impl JsEvmWrapper { ) } + #[napi(ts_return_type = "Promise")] + pub fn simulate( + &mut self, + node_env: Env, + tx_ctx: JsTransactionSimulateContext, + ) -> Result { + let tx_ctx = TxSimulateContext::try_from(tx_ctx)?; + node_env.execute_tokio_future( + Self::simulate_async(self.evm.clone(), tx_ctx), + |&mut node_env, result| Ok(result::JsSimulateResult::new(&node_env, result)?), + ) + } + #[napi(ts_return_type = "Promise")] pub fn initialize_genesis( &mut self, @@ -1601,6 +1633,19 @@ impl JsEvmWrapper { } } + async fn simulate_async( + evm: Arc>, + tx_ctx: TxSimulateContext, + ) -> Result { + let mut lock = evm.lock().await; + let result = lock.simulate(tx_ctx); + + match result { + Ok(result) => Result::Ok(result), + Err(err) => Result::Err(serde::de::Error::custom(err)), + } + } + async fn get_account_info_async( evm: Arc>, address: Address, diff --git a/packages/evm/bindings/src/result.rs b/packages/evm/bindings/src/result.rs index 789d607b15..fbc26594fc 100644 --- a/packages/evm/bindings/src/result.rs +++ b/packages/evm/bindings/src/result.rs @@ -25,6 +25,18 @@ impl JsProcessResult { } } +#[napi(object)] +pub struct JsSimulateResult { + pub receipt: JsTransactionReceipt, +} +impl JsSimulateResult { + pub fn new(node_env: &napi::Env, receipt: TxReceipt) -> anyhow::Result { + Ok(Self { + receipt: JsTransactionReceipt::new(node_env, receipt)?, + }) + } +} + #[napi(object)] pub struct JsCommitResult { pub dirty_accounts: Vec,