diff --git a/packages/typewriter-evm/src/harness.rs b/packages/typewriter-evm/src/harness.rs index da20e6c..b9d077e 100644 --- a/packages/typewriter-evm/src/harness.rs +++ b/packages/typewriter-evm/src/harness.rs @@ -149,7 +149,9 @@ struct ExecuteOk { #[serde(skip_serializing_if = "Option::is_none")] journal_id: Option, gas_used: u64, - gas_limit: u64, + // Only simulate estimates a gas limit; execute leaves it unset. + #[serde(skip_serializing_if = "Option::is_none")] + gas_limit: Option, output: String, access_list: Vec, slot_writes: Vec, @@ -386,7 +388,7 @@ impl EvmHarness { fn execute(&mut self, params: &ExecuteParams) -> Result { self.ensure_initialized()?; - let mut result = match self.run_two_pass(params, None) { + let mut result = match self.run_two_pass(params, None, false) { Ok(result) => result, Err(error) => { let _ = self.evm.finalize(); @@ -429,7 +431,7 @@ impl EvmHarness { } let cached_access_list = self.merge_journal_access_lists(¶ms.journal_ids); - let result = self.run_two_pass(¶ms.as_execute_params(), cached_access_list); + let result = self.run_two_pass(¶ms.as_execute_params(), cached_access_list, true); // Drop pass 2's journal-state writes so they never reach the DB. let _ = self.evm.finalize(); @@ -517,6 +519,11 @@ impl EvmHarness { // those addresses/slots pre-warmed so the metered gas matches what the // chain will charge for a tx broadcast with the discovered access list. // + // With `estimate_gas`, a successful pass 2 is followed by a gas-limit + // search and a final pass at that limit. Only simulate needs this: it + // builds the transaction that goes onchain. Execute only accepts the + // mutation into local state, so it skips the search (about 15 extra runs). + // // After pass 1, finalize drains the journal state and we discard it so // pass 2 starts from the same DB state. The alternative — // checkpoint_revert — doesn't work because transact_one calls commit_tx @@ -525,6 +532,7 @@ impl EvmHarness { &mut self, params: &ExecuteParams, cached_access_list: Option, + estimate_gas: bool, ) -> Result { let from = parse_address(¶ms.from)?; let to = parse_address(¶ms.to)?; @@ -590,6 +598,7 @@ impl EvmHarness { .map_err(|e| format!("transact (pass 2): {e:?}"))?; let gas_limit = match &result { + ExecutionResult::Success { .. } if !estimate_gas => None, ExecutionResult::Success { .. } => { self.evm.0.inspector.clear(); let _ = self.evm.finalize(); @@ -603,9 +612,11 @@ impl EvmHarness { .evm .inspect_one_tx(build_tx(tx_access_list.clone(), gas_limit)) .map_err(|e| format!("transact (final pass): {e:?}"))?; - gas_limit + Some(gas_limit) + } + ExecutionResult::Revert { .. } | ExecutionResult::Halt { .. } => { + estimate_gas.then(|| result.tx_gas_used()) } - ExecutionResult::Revert { .. } | ExecutionResult::Halt { .. } => result.tx_gas_used(), }; let (success, gas_used, output, revert_data) = match &result { diff --git a/packages/typewriter-evm/src/index.test.ts b/packages/typewriter-evm/src/index.test.ts index d0c643d..93bc88b 100644 --- a/packages/typewriter-evm/src/index.test.ts +++ b/packages/typewriter-evm/src/index.test.ts @@ -94,6 +94,8 @@ test("execute against counter succeeds and returns a journal id", async () => { expect(exec.success).toBe(true); expect(exec.journal_id).toBe(1); expect(exec.revert_data).toBeUndefined(); + // Only simulate estimates a gas limit. + expect(exec).not.toHaveProperty("gas_limit"); expect(exec.slot_writes).toEqual([ { address: COUNTER_ADDR, diff --git a/packages/typewriter-evm/src/index.ts b/packages/typewriter-evm/src/index.ts index f3a80cb..949e2fa 100644 --- a/packages/typewriter-evm/src/index.ts +++ b/packages/typewriter-evm/src/index.ts @@ -12,6 +12,7 @@ import type { Request, Response, SimulateParams, + SimulateResult, } from "./types"; export type { @@ -24,6 +25,7 @@ export type { ReadStorageParams, ReadStorageResult, SimulateParams, + SimulateResult, Spec, } from "./types"; @@ -76,7 +78,7 @@ export type EVM = { ) => Effect.Effect; readonly simulate: ( params: SimulateParams, - ) => Effect.Effect; + ) => Effect.Effect; readonly readStorage: ( params: ReadStorageParams, ) => Effect.Effect; @@ -175,7 +177,7 @@ export const createEVM = (): Effect.Effect => params, })), simulate: (params) => - call("simulate", (id) => ({ + call("simulate", (id) => ({ method: "simulate", id, params, diff --git a/packages/typewriter-evm/src/types.ts b/packages/typewriter-evm/src/types.ts index 9030eba..d245b70 100644 --- a/packages/typewriter-evm/src/types.ts +++ b/packages/typewriter-evm/src/types.ts @@ -44,7 +44,6 @@ export type ExecuteResult = { success: boolean; journal_id?: number; gas_used: number; - gas_limit: number; output: Hex.Hex; access_list: { address: Address.Address; storageKeys: Hex.Hex[] }[]; slot_writes: { @@ -57,6 +56,12 @@ export type ExecuteResult = { revert_data?: Hex.Hex; }; +// Simulate also estimates the smallest gas limit that lets the call succeed. +// Execute skips that search because accepting a mutation does not need it. +export type SimulateResult = ExecuteResult & { + gas_limit: number; +}; + export type ReadStorageParams = { address: Address.Address; slots: Hex.Hex[];