@terrariumlabs/evm 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -1
- package/README.md +7 -1
- package/package.json +2 -2
- package/pkg/terrarium_evm.d.ts +9 -0
- package/pkg/terrarium_evm.js +34 -4
- package/pkg/terrarium_evm_bg.wasm +0 -0
- package/pkg/terrarium_evm_bg.wasm.d.ts +1 -0
- package/src/lib.rs +108 -17
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "terrarium-evm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "revm compiled to WebAssembly as a Terrarium execution backend"
|
|
6
6
|
license = "MIT"
|
|
@@ -23,3 +23,4 @@ getrandom = { version = "0.2", features = ["js"] }
|
|
|
23
23
|
opt-level = 3
|
|
24
24
|
lto = true
|
|
25
25
|
codegen-units = 1
|
|
26
|
+
panic = "abort" # no unwinding tables: smaller wasm; a panic is a trap either way in wasm
|
package/README.md
CHANGED
|
@@ -13,8 +13,14 @@ It executes one transaction per call. Everything else stays in JavaScript: accou
|
|
|
13
13
|
reverts, blocks and receipts, persistence, fork recording. The engine asks the host for what it reads and returns a
|
|
14
14
|
state diff to apply:
|
|
15
15
|
|
|
16
|
+
Two entry points: `run(host, request)` executes one transaction and returns its state diff; `estimate(host, request)` runs
|
|
17
|
+
reth's gas estimation (a run at the cap, the optimistic 64/63 probe, a bisection from ~3× the gas used, within 1.5 %) in one
|
|
18
|
+
call against a read cache, committing nothing. Analysed bytecode is cached by code hash across calls: `host.account(address,
|
|
19
|
+
wantCode)` may omit `code` when `wantCode` is false (never answer `'0x'` for a contract). Built with `wasm-opt -O3` and
|
|
20
|
+
`panic = "abort"`: 1.29 MB, 463 KB gzipped.
|
|
21
|
+
|
|
16
22
|
```js
|
|
17
|
-
import init, { run, version } from '@terrariumlabs/evm';
|
|
23
|
+
import init, { run, estimate, version } from '@terrariumlabs/evm';
|
|
18
24
|
await init({ module_or_path: wasmBytesOrUrl });
|
|
19
25
|
const result = JSON.parse(run(host, JSON.stringify({ tx, block, cfg })));
|
|
20
26
|
// result: { success, reason, gasUsed, gasRefunded, output, created, logs, state, sloads }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrariumlabs/evm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "revm compiled to WebAssembly: the fast execution backend for Terrarium",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"Cargo.toml"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "cargo build --release --target wasm32-unknown-unknown && wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/terrarium_evm.wasm"
|
|
46
|
+
"build": "cargo build --release --target wasm32-unknown-unknown && wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/terrarium_evm.wasm && wasm-opt -O3 --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --enable-mutable-globals -o pkg/terrarium_evm_bg.wasm pkg/terrarium_evm_bg.wasm && ls -la pkg/terrarium_evm_bg.wasm"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/pkg/terrarium_evm.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Estimate the gas for a transaction (same request shape as `run`; `tx.gasLimit` is the cap, usually the block gas
|
|
6
|
+
* limit). reth's algorithm, all inside wasm: one run at the cap (a failure there is the answer: the tx reverts), the
|
|
7
|
+
* optimistic `(used + refunded + stipend) · 64/63` probe, then bisection between `used - 1` and the best known limit,
|
|
8
|
+
* starting at `min(3 · used, mid)`, stopping within 1.5 %. Returns a JSON EstimateResult. Throws `missing` like `run`.
|
|
9
|
+
*/
|
|
10
|
+
export function estimate(host: any, request: string): string;
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* Execute one transaction. `request` is a JSON string (RunRequest); returns a JSON string (RunResult).
|
|
6
14
|
* Throws a string starting with `missing` when the host could not provide some state (re-run after fetching), and a
|
|
@@ -14,6 +22,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
|
|
14
22
|
|
|
15
23
|
export interface InitOutput {
|
|
16
24
|
readonly memory: WebAssembly.Memory;
|
|
25
|
+
readonly estimate: (a: any, b: number, c: number) => [number, number, number, number];
|
|
17
26
|
readonly run: (a: any, b: number, c: number) => [number, number, number, number];
|
|
18
27
|
readonly version: () => [number, number];
|
|
19
28
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
package/pkg/terrarium_evm.js
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
/* @ts-self-types="./terrarium_evm.d.ts" */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Estimate the gas for a transaction (same request shape as `run`; `tx.gasLimit` is the cap, usually the block gas
|
|
5
|
+
* limit). reth's algorithm, all inside wasm: one run at the cap (a failure there is the answer: the tx reverts), the
|
|
6
|
+
* optimistic `(used + refunded + stipend) · 64/63` probe, then bisection between `used - 1` and the best known limit,
|
|
7
|
+
* starting at `min(3 · used, mid)`, stopping within 1.5 %. Returns a JSON EstimateResult. Throws `missing` like `run`.
|
|
8
|
+
* @param {any} host
|
|
9
|
+
* @param {string} request
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function estimate(host, request) {
|
|
13
|
+
let deferred3_0;
|
|
14
|
+
let deferred3_1;
|
|
15
|
+
try {
|
|
16
|
+
const ptr0 = passStringToWasm0(request, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
17
|
+
const len0 = WASM_VECTOR_LEN;
|
|
18
|
+
const ret = wasm.estimate(host, ptr0, len0);
|
|
19
|
+
var ptr2 = ret[0];
|
|
20
|
+
var len2 = ret[1];
|
|
21
|
+
if (ret[3]) {
|
|
22
|
+
ptr2 = 0; len2 = 0;
|
|
23
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
24
|
+
}
|
|
25
|
+
deferred3_0 = ptr2;
|
|
26
|
+
deferred3_1 = len2;
|
|
27
|
+
return getStringFromWasm0(ptr2, len2);
|
|
28
|
+
} finally {
|
|
29
|
+
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
/**
|
|
4
34
|
* Execute one transaction. `request` is a JSON string (RunRequest); returns a JSON string (RunResult).
|
|
5
35
|
* Throws a string starting with `missing` when the host could not provide some state (re-run after fetching), and a
|
|
@@ -78,11 +108,11 @@ function __wbg_get_imports() {
|
|
|
78
108
|
__wbg___wbindgen_throw_bb96b2010945f0bc: function(arg0, arg1) {
|
|
79
109
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
80
110
|
},
|
|
81
|
-
|
|
82
|
-
const ret = arg0.account(getStringFromWasm0(arg1, arg2));
|
|
111
|
+
__wbg_account_56bab912b202698f: function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
|
112
|
+
const ret = arg0.account(getStringFromWasm0(arg1, arg2), arg3 !== 0);
|
|
83
113
|
return ret;
|
|
84
114
|
}, arguments); },
|
|
85
|
-
|
|
115
|
+
__wbg_blockHash_767bc4c470457615: function() { return handleError(function (arg0, arg1) {
|
|
86
116
|
const ret = arg0.blockHash(arg1);
|
|
87
117
|
return ret;
|
|
88
118
|
}, arguments); },
|
|
@@ -90,7 +120,7 @@ function __wbg_get_imports() {
|
|
|
90
120
|
const ret = Reflect.get(arg0, arg1);
|
|
91
121
|
return ret;
|
|
92
122
|
}, arguments); },
|
|
93
|
-
|
|
123
|
+
__wbg_storage_cd38fcce66b0ec85: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
|
94
124
|
const ret = arg0.storage(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
|
95
125
|
return ret;
|
|
96
126
|
}, arguments); },
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const estimate: (a: any, b: number, c: number) => [number, number, number, number];
|
|
4
5
|
export const run: (a: any, b: number, c: number) => [number, number, number, number];
|
|
5
6
|
export const version: () => [number, number];
|
|
6
7
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
package/src/lib.rs
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
//! only executes: it asks the host for whatever it reads, and returns the result plus the state diff to apply.
|
|
5
5
|
//! If the host cannot answer synchronously (fork mode: the slot has to be fetched from a node), it throws an error
|
|
6
6
|
//! marked `missing`; execution aborts, the host fetches, and re-runs. Reads are recorded, so re-runs are exact.
|
|
7
|
+
//!
|
|
8
|
+
//! Two entry points: `run` executes one transaction and returns its state diff; `estimate` runs reth's gas estimation
|
|
9
|
+
//! (first run at the cap, the optimistic 64/63 probe, then a bisection that starts near 3× the gas used) entirely in
|
|
10
|
+
//! here, against a read cache, so the host answers each account and slot once and JavaScript makes one call.
|
|
11
|
+
//! Analysed bytecode is cached by code hash across calls: the host is asked for a contract's code once, not per read.
|
|
12
|
+
use std::cell::RefCell;
|
|
7
13
|
use std::collections::HashMap;
|
|
8
14
|
use std::str::FromStr;
|
|
9
15
|
|
|
@@ -17,7 +23,7 @@ use revm::interpreter::interpreter_types::{InputsTr, Jumps, StackTr};
|
|
|
17
23
|
#[allow(unused_imports)] use StackTr as _StackTrUsed;
|
|
18
24
|
use revm::interpreter::Interpreter;
|
|
19
25
|
use revm::primitives::hardfork::SpecId;
|
|
20
|
-
use revm::primitives::{Address, Bytes, TxKind, B256, U256};
|
|
26
|
+
use revm::primitives::{Address, Bytes, TxKind, B256, KECCAK_EMPTY, U256};
|
|
21
27
|
use revm::state::{AccountInfo, Bytecode};
|
|
22
28
|
use serde::{Deserialize, Serialize};
|
|
23
29
|
use wasm_bindgen::prelude::*;
|
|
@@ -26,9 +32,10 @@ use wasm_bindgen::prelude::*;
|
|
|
26
32
|
#[wasm_bindgen]
|
|
27
33
|
extern "C" {
|
|
28
34
|
pub type Host;
|
|
29
|
-
/// -> null (no account) | { balance, nonce, codeHash, code } as hex strings.
|
|
35
|
+
/// -> null (no account) | { balance, nonce, codeHash, code? } as hex strings. `want_code` false: the host may omit
|
|
36
|
+
/// `code` (this side has it cached by codeHash); a host that always includes it works too. Throws { missing: true } to abort.
|
|
30
37
|
#[wasm_bindgen(method, catch)]
|
|
31
|
-
fn account(this: &Host, address: &str) -> Result<JsValue, JsValue>;
|
|
38
|
+
fn account(this: &Host, address: &str, want_code: bool) -> Result<JsValue, JsValue>;
|
|
32
39
|
/// -> 32-byte hex
|
|
33
40
|
#[wasm_bindgen(method, catch)]
|
|
34
41
|
fn storage(this: &Host, address: &str, slot: &str) -> Result<JsValue, JsValue>;
|
|
@@ -57,26 +64,44 @@ fn parse_b256(s: &str) -> Result<B256, HostError> { B256::from_str(s).map_err(|e
|
|
|
57
64
|
fn parse_addr(s: &str) -> Result<Address, HostError> { Address::from_str(s).map_err(|e| HostError::Other(format!("bad address {s}: {e}"))) }
|
|
58
65
|
fn parse_bytes(s: &str) -> Result<Bytes, HostError> { Bytes::from_str(s).map_err(|e| HostError::Other(format!("bad bytes: {e}"))) }
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
// analysed bytecode by code hash, kept across calls (a dev chain has a few dozen contracts; cleared if it ever balloons)
|
|
68
|
+
thread_local! { static CODE_CACHE: RefCell<HashMap<B256, Bytecode>> = RefCell::new(HashMap::new()); }
|
|
69
|
+
const CODE_CACHE_MAX: usize = 4096;
|
|
70
|
+
|
|
71
|
+
/// revm's view of the world: every read goes to the host, cached for the duration of one call (`run`, or a whole
|
|
72
|
+
/// `estimate`: its runs share the cache, and nothing is ever committed, so the host is asked once per key).
|
|
61
73
|
struct HostDb<'a> { host: &'a Host, accounts: HashMap<Address, Option<AccountInfo>>, storage: HashMap<(Address, U256), U256> }
|
|
62
74
|
|
|
63
75
|
impl<'a> Database for HostDb<'a> {
|
|
64
76
|
type Error = HostError;
|
|
65
77
|
fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, HostError> {
|
|
66
78
|
if let Some(a) = self.accounts.get(&address) { return Ok(a.clone()); }
|
|
67
|
-
let
|
|
79
|
+
let key = format!("{address:?}");
|
|
80
|
+
let v = self.host.account(&key, false).map_err(js_err)?;
|
|
68
81
|
let info = if v.is_null() || v.is_undefined() { None } else {
|
|
69
|
-
let code = parse_bytes(&js_str(&v, "code")?)?;
|
|
70
82
|
let code_hash = parse_b256(&js_str(&v, "codeHash")?)?;
|
|
71
83
|
let mut info = AccountInfo::default();
|
|
72
84
|
info.balance = parse_u256(&js_str(&v, "balance")?)?; info.nonce = parse_u256(&js_str(&v, "nonce")?)?.to::<u64>(); info.code_hash = code_hash;
|
|
73
|
-
if
|
|
85
|
+
if code_hash != KECCAK_EMPTY {
|
|
86
|
+
let cached = CODE_CACHE.with(|c| c.borrow().get(&code_hash).cloned());
|
|
87
|
+
let bytecode = match cached {
|
|
88
|
+
Some(b) => b,
|
|
89
|
+
None => {
|
|
90
|
+
// not cached yet: take the code from this answer if the host sent it, else ask for it once
|
|
91
|
+
let code = match js_str(&v, "code") { Ok(s) if s.len() > 2 => s, _ => js_str(&self.host.account(&key, true).map_err(js_err)?, "code")? };
|
|
92
|
+
let b = Bytecode::new_raw(parse_bytes(&code)?);
|
|
93
|
+
CODE_CACHE.with(|c| { let mut c = c.borrow_mut(); if c.len() >= CODE_CACHE_MAX { c.clear(); } c.insert(code_hash, b.clone()); });
|
|
94
|
+
b
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
info.code = Some(bytecode); // code comes with the account: revm never needs code_by_hash
|
|
98
|
+
}
|
|
74
99
|
Some(info)
|
|
75
100
|
};
|
|
76
101
|
self.accounts.insert(address, info.clone());
|
|
77
102
|
Ok(info)
|
|
78
103
|
}
|
|
79
|
-
fn code_by_hash(&mut self,
|
|
104
|
+
fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, HostError> { Ok(CODE_CACHE.with(|c| c.borrow().get(&code_hash).cloned()).unwrap_or_default()) }
|
|
80
105
|
fn storage(&mut self, address: Address, index: U256) -> Result<U256, HostError> {
|
|
81
106
|
if let Some(v) = self.storage.get(&(address, index)) { return Ok(*v); }
|
|
82
107
|
let v = self.host.storage(&format!("{address:?}"), &format!("{index:#066x}")).map_err(js_err)?;
|
|
@@ -124,6 +149,11 @@ pub struct AccountOut { address: String, deleted: bool, balance: String, nonce:
|
|
|
124
149
|
#[derive(Serialize)]
|
|
125
150
|
#[serde(rename_all = "camelCase")]
|
|
126
151
|
pub struct RunResult { success: bool, reason: String, gas_used: u64, gas_refunded: u64, output: String, created: Option<String>, logs: Vec<LogOut>, state: Vec<AccountOut>, sloads: Vec<(String, String)> }
|
|
152
|
+
/// `estimate`'s answer: the gas limit to use, how many runs it took, and the first run's outcome (a revert at the cap
|
|
153
|
+
/// means the transaction fails regardless of gas: `success` false, `reason` / `output` say why, like a receipt would).
|
|
154
|
+
#[derive(Serialize)]
|
|
155
|
+
#[serde(rename_all = "camelCase")]
|
|
156
|
+
pub struct EstimateResult { gas: u64, runs: u32, success: bool, reason: String, output: String, gas_used: u64 }
|
|
127
157
|
|
|
128
158
|
fn spec_of(name: Option<&str>) -> SpecId {
|
|
129
159
|
match name.map(|s| s.to_ascii_lowercase()).as_deref() { Some("prague") => SpecId::PRAGUE, Some("shanghai") => SpecId::SHANGHAI, Some("merge") | Some("paris") => SpecId::MERGE, Some("osaka") => SpecId::OSAKA, _ => SpecId::CANCUN }
|
|
@@ -143,7 +173,18 @@ pub fn run(host: &Host, request: &str) -> Result<String, JsValue> {
|
|
|
143
173
|
run_inner(host, req).map_err(|e| JsValue::from_str(&e))
|
|
144
174
|
}
|
|
145
175
|
|
|
146
|
-
|
|
176
|
+
/// Estimate the gas for a transaction (same request shape as `run`; `tx.gasLimit` is the cap, usually the block gas
|
|
177
|
+
/// limit). reth's algorithm, all inside wasm: one run at the cap (a failure there is the answer: the tx reverts), the
|
|
178
|
+
/// optimistic `(used + refunded + stipend) · 64/63` probe, then bisection between `used - 1` and the best known limit,
|
|
179
|
+
/// starting at `min(3 · used, mid)`, stopping within 1.5 %. Returns a JSON EstimateResult. Throws `missing` like `run`.
|
|
180
|
+
#[wasm_bindgen]
|
|
181
|
+
pub fn estimate(host: &Host, request: &str) -> Result<String, JsValue> {
|
|
182
|
+
let req: RunRequest = serde_json::from_str(request).map_err(|e| JsValue::from_str(&format!("bad request: {e}")))?;
|
|
183
|
+
estimate_inner(host, req).map_err(|e| JsValue::from_str(&e))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// the environments a request describes
|
|
187
|
+
fn envs(req: &RunRequest) -> Result<(BlockEnv, TxEnv, CfgEnv), String> {
|
|
147
188
|
let h = |e: HostError| e.to_string();
|
|
148
189
|
let block = BlockEnv {
|
|
149
190
|
number: parse_u256(&req.block.number).map_err(h)?,
|
|
@@ -178,18 +219,26 @@ fn run_inner(host: &Host, req: RunRequest) -> Result<String, String> {
|
|
|
178
219
|
cfg.disable_eip3607 = req.cfg.skip_eip3607; // simulations may originate from a contract address
|
|
179
220
|
cfg.limit_contract_code_size = Some(usize::MAX); // allowUnlimitedContractSize, like the JS engine
|
|
180
221
|
cfg.limit_contract_initcode_size = Some(usize::MAX);
|
|
222
|
+
Ok((block, tx, cfg))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// the errors of a transact call, as the host sees them
|
|
226
|
+
fn evm_error(e: EVMError<HostError>) -> String {
|
|
227
|
+
match e {
|
|
228
|
+
EVMError::Database(HostError::Missing) => "missing".into(),
|
|
229
|
+
EVMError::Database(e) => format!("host: {e}"),
|
|
230
|
+
EVMError::Transaction(e) => format!("invalid: {e:?}"),
|
|
231
|
+
EVMError::Header(e) => format!("invalid header: {e:?}"),
|
|
232
|
+
e => format!("evm: {e:?}"),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
181
235
|
|
|
236
|
+
fn run_inner(host: &Host, req: RunRequest) -> Result<String, String> {
|
|
237
|
+
let (block, tx, cfg) = envs(&req)?;
|
|
182
238
|
let db = HostDb { host, accounts: HashMap::new(), storage: HashMap::new() };
|
|
183
239
|
let ctx = Context::mainnet().with_db(db).with_block(block).with_cfg(cfg);
|
|
184
240
|
let mut evm = ctx.build_mainnet_with_inspector(SloadTracer { on: req.cfg.trace_sloads, reads: Vec::new() });
|
|
185
|
-
let res =
|
|
186
|
-
Ok(r) => r,
|
|
187
|
-
Err(EVMError::Database(HostError::Missing)) => return Err("missing".into()),
|
|
188
|
-
Err(EVMError::Database(e)) => return Err(format!("host: {e}")),
|
|
189
|
-
Err(EVMError::Transaction(e)) => return Err(format!("invalid: {e:?}")),
|
|
190
|
-
Err(EVMError::Header(e)) => return Err(format!("invalid header: {e:?}")),
|
|
191
|
-
Err(e) => return Err(format!("evm: {e:?}")),
|
|
192
|
-
};
|
|
241
|
+
let res = evm.inspect_tx(tx).map_err(evm_error)?;
|
|
193
242
|
let sloads = evm.inspector.reads.iter().map(|(a, s)| (format!("{a:?}"), format!("{s:#066x}"))).collect();
|
|
194
243
|
|
|
195
244
|
let (success, reason, gas, logs, output, created) = match res.result {
|
|
@@ -213,3 +262,45 @@ fn run_inner(host: &Host, req: RunRequest) -> Result<String, String> {
|
|
|
213
262
|
};
|
|
214
263
|
serde_json::to_string(&out).map_err(|e| e.to_string())
|
|
215
264
|
}
|
|
265
|
+
|
|
266
|
+
/// reth's estimation constants: the error ratio the bisection stops at, and the call stipend added to the optimistic probe
|
|
267
|
+
const ESTIMATE_GAS_ERROR_RATIO: f64 = 0.015;
|
|
268
|
+
const CALL_STIPEND_GAS: u64 = 2_300;
|
|
269
|
+
|
|
270
|
+
fn estimate_inner(host: &Host, req: RunRequest) -> Result<String, String> {
|
|
271
|
+
let (block, tx, cfg) = envs(&req)?;
|
|
272
|
+
let db = HostDb { host, accounts: HashMap::new(), storage: HashMap::new() };
|
|
273
|
+
let ctx = Context::mainnet().with_db(db).with_block(block).with_cfg(cfg);
|
|
274
|
+
let mut evm = ctx.build_mainnet_with_inspector(SloadTracer::default());
|
|
275
|
+
let mut runs: u32 = 0;
|
|
276
|
+
// nothing is committed between runs, so every run starts from the same state and the read cache holds
|
|
277
|
+
let mut exec = |gas_limit: u64| -> Result<ExecutionResult, String> {
|
|
278
|
+
runs += 1;
|
|
279
|
+
let mut t = tx.clone(); t.gas_limit = gas_limit;
|
|
280
|
+
Ok(evm.inspect_tx(t).map_err(evm_error)?.result)
|
|
281
|
+
};
|
|
282
|
+
let cap = tx.gas_limit;
|
|
283
|
+
let first = exec(cap)?;
|
|
284
|
+
let (gas_used, refunded) = match &first {
|
|
285
|
+
ExecutionResult::Success { gas, .. } => (gas.tx_gas_used(), gas.final_refunded()),
|
|
286
|
+
ExecutionResult::Revert { gas, output, .. } => {
|
|
287
|
+
return serde_json::to_string(&EstimateResult { gas: 0, runs, success: false, reason: "revert".into(), output: format!("0x{}", hex::encode(output)), gas_used: gas.tx_gas_used() }).map_err(|e| e.to_string());
|
|
288
|
+
}
|
|
289
|
+
ExecutionResult::Halt { reason, gas, .. } => {
|
|
290
|
+
return serde_json::to_string(&EstimateResult { gas: 0, runs, success: false, reason: format!("{reason:?}"), output: "0x".into(), gas_used: gas.tx_gas_used() }).map_err(|e| e.to_string());
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
let mut highest = cap;
|
|
294
|
+
let mut lowest = gas_used.saturating_sub(1);
|
|
295
|
+
// the optimistic probe: what a call needs when every frame keeps its 1/64th
|
|
296
|
+
let optimistic = (gas_used + refunded + CALL_STIPEND_GAS) * 64 / 63;
|
|
297
|
+
if optimistic < highest { if exec(optimistic)?.is_success() { highest = optimistic; } else { lowest = optimistic; } }
|
|
298
|
+
// bisection, starting near 3× the gas used rather than in the middle of a 30M range (reth)
|
|
299
|
+
let mut mid = std::cmp::min(gas_used.saturating_mul(3), (highest + lowest) / 2);
|
|
300
|
+
while highest - lowest > 1 {
|
|
301
|
+
if ((highest - lowest) as f64) / (highest as f64) < ESTIMATE_GAS_ERROR_RATIO { break; }
|
|
302
|
+
if exec(mid)?.is_success() { highest = mid; } else { lowest = mid; }
|
|
303
|
+
mid = (highest + lowest) / 2;
|
|
304
|
+
}
|
|
305
|
+
serde_json::to_string(&EstimateResult { gas: highest, runs, success: true, reason: "ok".into(), output: "0x".into(), gas_used }).map_err(|e| e.to_string())
|
|
306
|
+
}
|