@terrariumlabs/evm 0.3.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 ADDED
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "terrarium-evm"
3
+ version = "0.3.0"
4
+ edition = "2021"
5
+ description = "revm compiled to WebAssembly as a Terrarium execution backend"
6
+ license = "MIT"
7
+
8
+ [lib]
9
+ crate-type = ["cdylib", "rlib"]
10
+
11
+ [dependencies]
12
+ # no C dependencies (secp256k1 / c-kzg / blst are replaced by their pure-Rust fallbacks), so it builds for wasm32
13
+ revm = { version = "43", default-features = false, features = ["std", "optional_balance_check", "optional_block_gas_limit", "optional_no_base_fee", "optional_eip3607"] }
14
+ wasm-bindgen = "0.2"
15
+ js-sys = "0.3"
16
+ serde = { version = "1", features = ["derive"] }
17
+ serde_json = "1"
18
+ hex = "0.4"
19
+ # wasm32-unknown-unknown has no OS entropy source: route getrandom (pulled in transitively) through the browser/Node APIs
20
+ getrandom = { version = "0.2", features = ["js"] }
21
+
22
+ [profile.release]
23
+ opt-level = 3
24
+ lto = true
25
+ codegen-units = 1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Martin (damarnez)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @terrariumlabs/evm
2
+
3
+ [revm](https://github.com/bluealloy/revm) 43 compiled to WebAssembly, as the execution engine of Terrarium. About 1.5 MB
4
+ of wasm, no C dependencies (k256 for secp256k1, arkworks for bn254/bls12-381, pure-Rust KZG), built with wasm-bindgen
5
+ `--target web`. `pkg/` is committed so JavaScript users need no Rust toolchain.
6
+
7
+ ## What it does, and only that
8
+ It executes one transaction per call. Everything else stays in JavaScript: accounts, code and storage, checkpoints and
9
+ reverts, blocks and receipts, persistence, fork recording. The engine asks the host for what it reads and returns a
10
+ state diff to apply:
11
+
12
+ ```js
13
+ import init, { run, version } from '@terrariumlabs/evm';
14
+ await init({ module_or_path: wasmBytesOrUrl });
15
+ const result = JSON.parse(run(host, JSON.stringify({ tx, block, cfg })));
16
+ // result: { success, reason, gasUsed, gasRefunded, output, created, logs, state, sloads }
17
+ ```
18
+ `host` is a plain object with synchronous methods `account(address)`, `storage(address, slot)`, `blockHash(number)`.
19
+ If the host cannot answer synchronously it throws `{ missing: true }`; `run` throws `"missing"`, the caller loads the
20
+ state and calls again. Reads are recorded, so re-runs are exact. `cfg.traceSloads` returns every SLOAD (address, slot)
21
+ — how Terrarium's `deal` finds a token's balance slot. Field-by-field schema: [docs/api.md](../../docs/api.md).
22
+
23
+ ## Build
24
+ ```bash
25
+ rustup target add wasm32-unknown-unknown
26
+ cargo install wasm-bindgen-cli # 0.2.127 at the time of writing; must match the wasm-bindgen crate version
27
+ npm run build # cargo build --release --target wasm32-unknown-unknown && wasm-bindgen --target web --out-dir pkg
28
+ node smoke.mjs # deploy PEPE, call balanceOf, trace the balance slot — PASS
29
+ ```
30
+ `getrandom` is pinned with its `js` feature: wasm32-unknown-unknown has no OS entropy source.
31
+
32
+ ## Fidelity and speed
33
+ `npm run test:uniswap` (repo root) runs a 14-transaction Uniswap V2 scenario on this engine and on Anvil: every
34
+ transaction hash, receipt, log, call result and revert payload is byte-identical, every block's header hash and roots
35
+ recompute from the RPC output. The scenario takes ≈135 ms here (≈35 ms inside the wasm) versus ≈75 ms on native Anvil.
36
+ `test/unit/wasm.test.mjs` pins the `run` / `host` contract described above. Since terrarium 0.3 this is the only
37
+ execution engine.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@terrariumlabs/evm",
3
+ "version": "0.3.0",
4
+ "description": "revm compiled to WebAssembly: the fast execution backend for Terrarium",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/damarnez/terrarium.git",
10
+ "directory": "packages/terrarium-evm"
11
+ },
12
+ "homepage": "https://github.com/damarnez/terrarium#readme",
13
+ "bugs": "https://github.com/damarnez/terrarium/issues",
14
+ "keywords": [
15
+ "ethereum",
16
+ "evm",
17
+ "revm",
18
+ "wasm",
19
+ "eip-1193",
20
+ "eip-6963",
21
+ "wallet",
22
+ "dapp",
23
+ "testing",
24
+ "simulator",
25
+ "anvil",
26
+ "rust",
27
+ "wasm-bindgen"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "main": "pkg/terrarium_evm.js",
36
+ "exports": {
37
+ ".": "./pkg/terrarium_evm.js",
38
+ "./wasm": "./pkg/terrarium_evm_bg.wasm"
39
+ },
40
+ "files": [
41
+ "pkg",
42
+ "src",
43
+ "Cargo.toml"
44
+ ],
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"
47
+ }
48
+ }
@@ -0,0 +1,49 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Execute one transaction. `request` is a JSON string (RunRequest); returns a JSON string (RunResult).
6
+ * Throws a string starting with `missing` when the host could not provide some state (re-run after fetching), and a
7
+ * string starting with `invalid:` for a transaction the node would refuse (bad nonce, insufficient funds...).
8
+ */
9
+ export function run(host: any, request: string): string;
10
+
11
+ export function version(): string;
12
+
13
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
14
+
15
+ export interface InitOutput {
16
+ readonly memory: WebAssembly.Memory;
17
+ readonly run: (a: any, b: number, c: number) => [number, number, number, number];
18
+ readonly version: () => [number, number];
19
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
20
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
21
+ readonly __wbindgen_exn_store: (a: number) => void;
22
+ readonly __externref_table_alloc: () => number;
23
+ readonly __wbindgen_externrefs: WebAssembly.Table;
24
+ readonly __externref_table_dealloc: (a: number) => void;
25
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
26
+ readonly __wbindgen_start: () => void;
27
+ }
28
+
29
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
30
+
31
+ /**
32
+ * Instantiates the given `module`, which can either be bytes or
33
+ * a precompiled `WebAssembly.Module`.
34
+ *
35
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
36
+ *
37
+ * @returns {InitOutput}
38
+ */
39
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
40
+
41
+ /**
42
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
43
+ * for everything else, calls `WebAssembly.instantiate` directly.
44
+ *
45
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
46
+ *
47
+ * @returns {Promise<InitOutput>}
48
+ */
49
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
@@ -0,0 +1,390 @@
1
+ /* @ts-self-types="./terrarium_evm.d.ts" */
2
+
3
+ /**
4
+ * Execute one transaction. `request` is a JSON string (RunRequest); returns a JSON string (RunResult).
5
+ * Throws a string starting with `missing` when the host could not provide some state (re-run after fetching), and a
6
+ * string starting with `invalid:` for a transaction the node would refuse (bad nonce, insufficient funds...).
7
+ * @param {any} host
8
+ * @param {string} request
9
+ * @returns {string}
10
+ */
11
+ export function run(host, request) {
12
+ let deferred3_0;
13
+ let deferred3_1;
14
+ try {
15
+ const ptr0 = passStringToWasm0(request, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
16
+ const len0 = WASM_VECTOR_LEN;
17
+ const ret = wasm.run(host, ptr0, len0);
18
+ var ptr2 = ret[0];
19
+ var len2 = ret[1];
20
+ if (ret[3]) {
21
+ ptr2 = 0; len2 = 0;
22
+ throw takeFromExternrefTable0(ret[2]);
23
+ }
24
+ deferred3_0 = ptr2;
25
+ deferred3_1 = len2;
26
+ return getStringFromWasm0(ptr2, len2);
27
+ } finally {
28
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * @returns {string}
34
+ */
35
+ export function version() {
36
+ let deferred1_0;
37
+ let deferred1_1;
38
+ try {
39
+ const ret = wasm.version();
40
+ deferred1_0 = ret[0];
41
+ deferred1_1 = ret[1];
42
+ return getStringFromWasm0(ret[0], ret[1]);
43
+ } finally {
44
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
45
+ }
46
+ }
47
+ function __wbg_get_imports() {
48
+ const import0 = {
49
+ __proto__: null,
50
+ __wbg___wbindgen_boolean_get_c9c83ebd41b34df3: function(arg0) {
51
+ const v = arg0;
52
+ const ret = typeof(v) === 'boolean' ? v : undefined;
53
+ return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
54
+ },
55
+ __wbg___wbindgen_debug_string_a57024b9c6e4a48b: function(arg0, arg1) {
56
+ const ret = debugString(arg1);
57
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
58
+ const len1 = WASM_VECTOR_LEN;
59
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
60
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
61
+ },
62
+ __wbg___wbindgen_is_null_7d13f41e1a2d5140: function(arg0) {
63
+ const ret = arg0 === null;
64
+ return ret;
65
+ },
66
+ __wbg___wbindgen_is_undefined_6cff064c44e0d823: function(arg0) {
67
+ const ret = arg0 === undefined;
68
+ return ret;
69
+ },
70
+ __wbg___wbindgen_string_get_d154f1e671052120: function(arg0, arg1) {
71
+ const obj = arg1;
72
+ const ret = typeof(obj) === 'string' ? obj : undefined;
73
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
74
+ var len1 = WASM_VECTOR_LEN;
75
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
76
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
77
+ },
78
+ __wbg___wbindgen_throw_bb96b2010945f0bc: function(arg0, arg1) {
79
+ throw new Error(getStringFromWasm0(arg0, arg1));
80
+ },
81
+ __wbg_account_865c9c68e728eee9: function() { return handleError(function (arg0, arg1, arg2) {
82
+ const ret = arg0.account(getStringFromWasm0(arg1, arg2));
83
+ return ret;
84
+ }, arguments); },
85
+ __wbg_blockHash_7e3aa4fc3b1cfb6b: function() { return handleError(function (arg0, arg1) {
86
+ const ret = arg0.blockHash(arg1);
87
+ return ret;
88
+ }, arguments); },
89
+ __wbg_get_971a0c45d172643f: function() { return handleError(function (arg0, arg1) {
90
+ const ret = Reflect.get(arg0, arg1);
91
+ return ret;
92
+ }, arguments); },
93
+ __wbg_storage_51594800c64c7b13: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
94
+ const ret = arg0.storage(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
95
+ return ret;
96
+ }, arguments); },
97
+ __wbindgen_cast_0000000000000001: function(arg0, arg1) {
98
+ // Cast intrinsic for `Ref(String) -> Externref`.
99
+ const ret = getStringFromWasm0(arg0, arg1);
100
+ return ret;
101
+ },
102
+ __wbindgen_init_externref_table: function() {
103
+ const table = wasm.__wbindgen_externrefs;
104
+ const offset = table.grow(4);
105
+ table.set(0, undefined);
106
+ table.set(offset + 0, undefined);
107
+ table.set(offset + 1, null);
108
+ table.set(offset + 2, true);
109
+ table.set(offset + 3, false);
110
+ },
111
+ };
112
+ return {
113
+ __proto__: null,
114
+ "./terrarium_evm_bg.js": import0,
115
+ };
116
+ }
117
+
118
+ function addToExternrefTable0(obj) {
119
+ const idx = wasm.__externref_table_alloc();
120
+ wasm.__wbindgen_externrefs.set(idx, obj);
121
+ return idx;
122
+ }
123
+
124
+ function debugString(val) {
125
+ // primitive types
126
+ const type = typeof val;
127
+ if (type == 'number' || type == 'boolean' || val == null) {
128
+ return `${val}`;
129
+ }
130
+ if (type == 'string') {
131
+ return `"${val}"`;
132
+ }
133
+ if (type == 'symbol') {
134
+ const description = val.description;
135
+ if (description == null) {
136
+ return 'Symbol';
137
+ } else {
138
+ return `Symbol(${description})`;
139
+ }
140
+ }
141
+ if (type == 'function') {
142
+ const name = val.name;
143
+ if (typeof name == 'string' && name.length > 0) {
144
+ return `Function(${name})`;
145
+ } else {
146
+ return 'Function';
147
+ }
148
+ }
149
+ // objects
150
+ if (Array.isArray(val)) {
151
+ const length = val.length;
152
+ let debug = '[';
153
+ if (length > 0) {
154
+ debug += debugString(val[0]);
155
+ }
156
+ for(let i = 1; i < length; i++) {
157
+ debug += ', ' + debugString(val[i]);
158
+ }
159
+ debug += ']';
160
+ return debug;
161
+ }
162
+ // Test for built-in
163
+ const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
164
+ let className;
165
+ if (builtInMatches && builtInMatches.length > 1) {
166
+ className = builtInMatches[1];
167
+ } else {
168
+ // Failed to match the standard '[object ClassName]'
169
+ return toString.call(val);
170
+ }
171
+ if (className == 'Object') {
172
+ // we're a user defined class or Object
173
+ // JSON.stringify avoids problems with cycles, and is generally much
174
+ // easier than looping through ownProperties of `val`.
175
+ try {
176
+ return 'Object(' + JSON.stringify(val) + ')';
177
+ } catch (_) {
178
+ return 'Object';
179
+ }
180
+ }
181
+ // errors
182
+ if (val instanceof Error) {
183
+ return `${val.name}: ${val.message}\n${val.stack}`;
184
+ }
185
+ // TODO we could test for more things here, like `Set`s and `Map`s.
186
+ return className;
187
+ }
188
+
189
+ let cachedDataViewMemory0 = null;
190
+ function getDataViewMemory0() {
191
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
192
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
193
+ }
194
+ return cachedDataViewMemory0;
195
+ }
196
+
197
+ function getStringFromWasm0(ptr, len) {
198
+ return decodeText(ptr >>> 0, len);
199
+ }
200
+
201
+ let cachedUint8ArrayMemory0 = null;
202
+ function getUint8ArrayMemory0() {
203
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
204
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
205
+ }
206
+ return cachedUint8ArrayMemory0;
207
+ }
208
+
209
+ function handleError(f, args) {
210
+ try {
211
+ return f.apply(this, args);
212
+ } catch (e) {
213
+ const idx = addToExternrefTable0(e);
214
+ wasm.__wbindgen_exn_store(idx);
215
+ }
216
+ }
217
+
218
+ function isLikeNone(x) {
219
+ return x === undefined || x === null;
220
+ }
221
+
222
+ function passStringToWasm0(arg, malloc, realloc) {
223
+ if (realloc === undefined) {
224
+ const buf = cachedTextEncoder.encode(arg);
225
+ const ptr = malloc(buf.length, 1) >>> 0;
226
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
227
+ WASM_VECTOR_LEN = buf.length;
228
+ return ptr;
229
+ }
230
+
231
+ let len = arg.length;
232
+ let ptr = malloc(len, 1) >>> 0;
233
+
234
+ const mem = getUint8ArrayMemory0();
235
+
236
+ let offset = 0;
237
+
238
+ for (; offset < len; offset++) {
239
+ const code = arg.charCodeAt(offset);
240
+ if (code > 0x7F) break;
241
+ mem[ptr + offset] = code;
242
+ }
243
+ if (offset !== len) {
244
+ if (offset !== 0) {
245
+ arg = arg.slice(offset);
246
+ }
247
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
248
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
249
+ const ret = cachedTextEncoder.encodeInto(arg, view);
250
+
251
+ offset += ret.written;
252
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
253
+ }
254
+
255
+ WASM_VECTOR_LEN = offset;
256
+ return ptr;
257
+ }
258
+
259
+ function takeFromExternrefTable0(idx) {
260
+ const value = wasm.__wbindgen_externrefs.get(idx);
261
+ wasm.__externref_table_dealloc(idx);
262
+ return value;
263
+ }
264
+
265
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
266
+ cachedTextDecoder.decode();
267
+ const MAX_SAFARI_DECODE_BYTES = 2146435072;
268
+ let numBytesDecoded = 0;
269
+ function decodeText(ptr, len) {
270
+ numBytesDecoded += len;
271
+ if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
272
+ cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
273
+ cachedTextDecoder.decode();
274
+ numBytesDecoded = len;
275
+ }
276
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
277
+ }
278
+
279
+ const cachedTextEncoder = new TextEncoder();
280
+
281
+ if (!('encodeInto' in cachedTextEncoder)) {
282
+ cachedTextEncoder.encodeInto = function (arg, view) {
283
+ const buf = cachedTextEncoder.encode(arg);
284
+ view.set(buf);
285
+ return {
286
+ read: arg.length,
287
+ written: buf.length
288
+ };
289
+ };
290
+ }
291
+
292
+ let WASM_VECTOR_LEN = 0;
293
+
294
+ let wasmModule, wasmInstance, wasm;
295
+ function __wbg_finalize_init(instance, module) {
296
+ wasmInstance = instance;
297
+ wasm = instance.exports;
298
+ wasmModule = module;
299
+ cachedDataViewMemory0 = null;
300
+ cachedUint8ArrayMemory0 = null;
301
+ wasm.__wbindgen_start();
302
+ return wasm;
303
+ }
304
+
305
+ async function __wbg_load(module, imports) {
306
+ if (typeof Response === 'function' && module instanceof Response) {
307
+ if (!module.ok) {
308
+ throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
309
+ }
310
+
311
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
312
+ try {
313
+ return await WebAssembly.instantiateStreaming(module, imports);
314
+ } catch (e) {
315
+ const validResponse = expectedResponseType(module.type);
316
+
317
+ if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
318
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
319
+
320
+ } else { throw e; }
321
+ }
322
+ }
323
+
324
+ const bytes = await module.arrayBuffer();
325
+ return await WebAssembly.instantiate(bytes, imports);
326
+ } else {
327
+ const instance = await WebAssembly.instantiate(module, imports);
328
+
329
+ if (instance instanceof WebAssembly.Instance) {
330
+ return { instance, module };
331
+ } else {
332
+ return instance;
333
+ }
334
+ }
335
+
336
+ function expectedResponseType(type) {
337
+ switch (type) {
338
+ case 'basic': case 'cors': case 'default': return true;
339
+ }
340
+ return false;
341
+ }
342
+ }
343
+
344
+ function initSync(module) {
345
+ if (wasm !== undefined) return wasm;
346
+
347
+
348
+ if (module !== undefined) {
349
+ if (Object.getPrototypeOf(module) === Object.prototype) {
350
+ ({module} = module)
351
+ } else {
352
+ console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
353
+ }
354
+ }
355
+
356
+ const imports = __wbg_get_imports();
357
+ if (!(module instanceof WebAssembly.Module)) {
358
+ module = new WebAssembly.Module(module);
359
+ }
360
+ const instance = new WebAssembly.Instance(module, imports);
361
+ return __wbg_finalize_init(instance, module);
362
+ }
363
+
364
+ async function __wbg_init(module_or_path) {
365
+ if (wasm !== undefined) return wasm;
366
+
367
+
368
+ if (module_or_path !== undefined) {
369
+ if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
370
+ ({module_or_path} = module_or_path)
371
+ } else {
372
+ console.warn('using deprecated parameters for the initialization function; pass a single object instead')
373
+ }
374
+ }
375
+
376
+ if (module_or_path === undefined) {
377
+ module_or_path = new URL('terrarium_evm_bg.wasm', import.meta.url);
378
+ }
379
+ const imports = __wbg_get_imports();
380
+
381
+ if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
382
+ module_or_path = fetch(module_or_path);
383
+ }
384
+
385
+ const { instance, module } = await __wbg_load(await module_or_path, imports);
386
+
387
+ return __wbg_finalize_init(instance, module);
388
+ }
389
+
390
+ export { initSync, __wbg_init as default };
Binary file
@@ -0,0 +1,13 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const run: (a: any, b: number, c: number) => [number, number, number, number];
5
+ export const version: () => [number, number];
6
+ export const __wbindgen_malloc: (a: number, b: number) => number;
7
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
8
+ export const __wbindgen_exn_store: (a: number) => void;
9
+ export const __externref_table_alloc: () => number;
10
+ export const __wbindgen_externrefs: WebAssembly.Table;
11
+ export const __externref_table_dealloc: (a: number) => void;
12
+ export const __wbindgen_free: (a: number, b: number, c: number) => void;
13
+ export const __wbindgen_start: () => void;
package/src/lib.rs ADDED
@@ -0,0 +1,215 @@
1
+ //! terrarium-evm: revm behind a tiny host interface, compiled to WebAssembly.
2
+ //!
3
+ //! The JavaScript side owns ALL state (accounts, code, storage, checkpoints, persistence, fork recording). This crate
4
+ //! only executes: it asks the host for whatever it reads, and returns the result plus the state diff to apply.
5
+ //! If the host cannot answer synchronously (fork mode: the slot has to be fetched from a node), it throws an error
6
+ //! marked `missing`; execution aborts, the host fetches, and re-runs. Reads are recorded, so re-runs are exact.
7
+ use std::collections::HashMap;
8
+ use std::str::FromStr;
9
+
10
+ use revm::context::{BlockEnv, CfgEnv, Context, TxEnv};
11
+ use revm::context_interface::result::{EVMError, ExecutionResult, Output};
12
+ use revm::database_interface::{DBErrorMarker, Database};
13
+ use revm::handler::{MainBuilder, MainContext};
14
+ use revm::inspector::{InspectEvm, Inspector};
15
+ use revm::interpreter::interpreter::EthInterpreter;
16
+ use revm::interpreter::interpreter_types::{InputsTr, Jumps, StackTr};
17
+ #[allow(unused_imports)] use StackTr as _StackTrUsed;
18
+ use revm::interpreter::Interpreter;
19
+ use revm::primitives::hardfork::SpecId;
20
+ use revm::primitives::{Address, Bytes, TxKind, B256, U256};
21
+ use revm::state::{AccountInfo, Bytecode};
22
+ use serde::{Deserialize, Serialize};
23
+ use wasm_bindgen::prelude::*;
24
+
25
+ // ---------------------------------------------------------------- the host (JavaScript) ------------------------------
26
+ #[wasm_bindgen]
27
+ extern "C" {
28
+ pub type Host;
29
+ /// -> null (no account) | { balance, nonce, codeHash, code } as hex strings. Throws { missing: true } to abort.
30
+ #[wasm_bindgen(method, catch)]
31
+ fn account(this: &Host, address: &str) -> Result<JsValue, JsValue>;
32
+ /// -> 32-byte hex
33
+ #[wasm_bindgen(method, catch)]
34
+ fn storage(this: &Host, address: &str, slot: &str) -> Result<JsValue, JsValue>;
35
+ /// -> 32-byte hex
36
+ #[wasm_bindgen(method, catch, js_name = blockHash)]
37
+ fn block_hash(this: &Host, number: f64) -> Result<JsValue, JsValue>;
38
+ }
39
+
40
+ #[derive(Debug)]
41
+ pub enum HostError { Missing, Other(String) }
42
+ impl std::fmt::Display for HostError {
43
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { HostError::Missing => write!(f, "missing state"), HostError::Other(s) => write!(f, "{s}") } }
44
+ }
45
+ impl std::error::Error for HostError {}
46
+ impl DBErrorMarker for HostError {}
47
+
48
+ fn js_err(e: JsValue) -> HostError {
49
+ let missing = js_sys::Reflect::get(&e, &JsValue::from_str("missing")).ok().and_then(|v| v.as_bool()).unwrap_or(false);
50
+ if missing { HostError::Missing } else { HostError::Other(format!("{e:?}")) }
51
+ }
52
+ fn js_str(v: &JsValue, key: &str) -> Result<String, HostError> {
53
+ js_sys::Reflect::get(v, &JsValue::from_str(key)).ok().and_then(|x| x.as_string()).ok_or_else(|| HostError::Other(format!("host returned no `{key}`")))
54
+ }
55
+ fn parse_u256(s: &str) -> Result<U256, HostError> { U256::from_str(s).map_err(|e| HostError::Other(format!("bad u256 {s}: {e}"))) }
56
+ fn parse_b256(s: &str) -> Result<B256, HostError> { B256::from_str(s).map_err(|e| HostError::Other(format!("bad b256 {s}: {e}"))) }
57
+ fn parse_addr(s: &str) -> Result<Address, HostError> { Address::from_str(s).map_err(|e| HostError::Other(format!("bad address {s}: {e}"))) }
58
+ fn parse_bytes(s: &str) -> Result<Bytes, HostError> { Bytes::from_str(s).map_err(|e| HostError::Other(format!("bad bytes: {e}"))) }
59
+
60
+ /// revm's view of the world: every read goes to the host, cached for the duration of one run.
61
+ struct HostDb<'a> { host: &'a Host, accounts: HashMap<Address, Option<AccountInfo>>, storage: HashMap<(Address, U256), U256> }
62
+
63
+ impl<'a> Database for HostDb<'a> {
64
+ type Error = HostError;
65
+ fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, HostError> {
66
+ if let Some(a) = self.accounts.get(&address) { return Ok(a.clone()); }
67
+ let v = self.host.account(&format!("{address:?}")).map_err(js_err)?;
68
+ let info = if v.is_null() || v.is_undefined() { None } else {
69
+ let code = parse_bytes(&js_str(&v, "code")?)?;
70
+ let code_hash = parse_b256(&js_str(&v, "codeHash")?)?;
71
+ let mut info = AccountInfo::default();
72
+ 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 !code.is_empty() { info.code = Some(Bytecode::new_raw(code)); } // code comes with the account: revm never needs code_by_hash
74
+ Some(info)
75
+ };
76
+ self.accounts.insert(address, info.clone());
77
+ Ok(info)
78
+ }
79
+ fn code_by_hash(&mut self, _code_hash: B256) -> Result<Bytecode, HostError> { Ok(Bytecode::default()) } // never reached: basic() carries the code
80
+ fn storage(&mut self, address: Address, index: U256) -> Result<U256, HostError> {
81
+ if let Some(v) = self.storage.get(&(address, index)) { return Ok(*v); }
82
+ let v = self.host.storage(&format!("{address:?}"), &format!("{index:#066x}")).map_err(js_err)?;
83
+ let value = parse_u256(&v.as_string().ok_or_else(|| HostError::Other("storage: not a string".into()))?)?;
84
+ self.storage.insert((address, index), value);
85
+ Ok(value)
86
+ }
87
+ fn block_hash(&mut self, number: u64) -> Result<B256, HostError> {
88
+ let v = self.host.block_hash(number as f64).map_err(js_err)?;
89
+ parse_b256(&v.as_string().ok_or_else(|| HostError::Other("blockHash: not a string".into()))?)
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------- SLOAD tracer (for `deal`'s slot discovery) ---------
94
+ #[derive(Default)]
95
+ struct SloadTracer { on: bool, reads: Vec<(Address, U256)> }
96
+ impl<CTX> Inspector<CTX, EthInterpreter> for SloadTracer {
97
+ fn step(&mut self, interp: &mut Interpreter<EthInterpreter>, _ctx: &mut CTX) {
98
+ if self.on && interp.bytecode.opcode() == 0x54 {
99
+ if let Some(slot) = interp.stack.top() { let slot = *slot; self.reads.push((interp.input.target_address(), slot)); }
100
+ }
101
+ }
102
+ }
103
+
104
+ // ---------------------------------------------------------------- request / response ----------------------------------
105
+ #[derive(Deserialize)]
106
+ #[serde(rename_all = "camelCase")]
107
+ pub struct RunRequest { tx: TxIn, block: BlockIn, cfg: CfgIn }
108
+ #[derive(Deserialize)]
109
+ #[serde(rename_all = "camelCase")]
110
+ pub struct TxIn { from: String, to: Option<String>, value: String, data: String, gas_limit: String, gas_price: String, priority_fee: Option<String>, nonce: Option<String>, tx_type: Option<u8> }
111
+ #[derive(Deserialize)]
112
+ #[serde(rename_all = "camelCase")]
113
+ pub struct BlockIn { number: String, timestamp: String, gas_limit: String, base_fee: String, coinbase: Option<String>, prev_randao: Option<String> }
114
+ #[derive(Deserialize, Default)]
115
+ #[serde(rename_all = "camelCase", default)]
116
+ pub struct CfgIn { chain_id: u64, spec: Option<String>, skip_balance: bool, skip_nonce: bool, skip_block_gas_limit: bool, no_base_fee: bool, skip_eip3607: bool, trace_sloads: bool }
117
+
118
+ #[derive(Serialize)]
119
+ #[serde(rename_all = "camelCase")]
120
+ pub struct LogOut { address: String, topics: Vec<String>, data: String }
121
+ #[derive(Serialize)]
122
+ #[serde(rename_all = "camelCase")]
123
+ pub struct AccountOut { address: String, deleted: bool, balance: String, nonce: String, code_hash: String, code: Option<String>, storage: Vec<(String, String)> }
124
+ #[derive(Serialize)]
125
+ #[serde(rename_all = "camelCase")]
126
+ 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)> }
127
+
128
+ fn spec_of(name: Option<&str>) -> SpecId {
129
+ 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 }
130
+ }
131
+ fn u64_of(s: &str) -> Result<u64, HostError> { Ok(parse_u256(s)?.to::<u64>()) }
132
+ fn u128_of(s: &str) -> Result<u128, HostError> { Ok(parse_u256(s)?.to::<u128>()) }
133
+
134
+ #[wasm_bindgen]
135
+ pub fn version() -> String { format!("terrarium-evm {} / revm 43", env!("CARGO_PKG_VERSION")) }
136
+
137
+ /// Execute one transaction. `request` is a JSON string (RunRequest); returns a JSON string (RunResult).
138
+ /// Throws a string starting with `missing` when the host could not provide some state (re-run after fetching), and a
139
+ /// string starting with `invalid:` for a transaction the node would refuse (bad nonce, insufficient funds...).
140
+ #[wasm_bindgen]
141
+ pub fn run(host: &Host, request: &str) -> Result<String, JsValue> {
142
+ let req: RunRequest = serde_json::from_str(request).map_err(|e| JsValue::from_str(&format!("bad request: {e}")))?;
143
+ run_inner(host, req).map_err(|e| JsValue::from_str(&e))
144
+ }
145
+
146
+ fn run_inner(host: &Host, req: RunRequest) -> Result<String, String> {
147
+ let h = |e: HostError| e.to_string();
148
+ let block = BlockEnv {
149
+ number: parse_u256(&req.block.number).map_err(h)?,
150
+ beneficiary: req.block.coinbase.as_deref().map(parse_addr).transpose().map_err(h)?.unwrap_or(Address::ZERO),
151
+ timestamp: parse_u256(&req.block.timestamp).map_err(h)?,
152
+ gas_limit: u64_of(&req.block.gas_limit).map_err(h)?,
153
+ basefee: u64_of(&req.block.base_fee).map_err(h)?,
154
+ difficulty: U256::ZERO,
155
+ prevrandao: Some(req.block.prev_randao.as_deref().map(parse_b256).transpose().map_err(h)?.unwrap_or(B256::ZERO)),
156
+ ..Default::default()
157
+ };
158
+ let caller = parse_addr(&req.tx.from).map_err(h)?;
159
+ let tx = TxEnv {
160
+ tx_type: req.tx.tx_type.unwrap_or(2),
161
+ caller,
162
+ gas_limit: u64_of(&req.tx.gas_limit).map_err(h)?,
163
+ gas_price: u128_of(&req.tx.gas_price).map_err(h)?,
164
+ gas_priority_fee: req.tx.priority_fee.as_deref().map(u128_of).transpose().map_err(h)?,
165
+ kind: match &req.tx.to { Some(to) => TxKind::Call(parse_addr(to).map_err(h)?), None => TxKind::Create },
166
+ value: parse_u256(&req.tx.value).map_err(h)?,
167
+ data: parse_bytes(&req.tx.data).map_err(h)?,
168
+ nonce: req.tx.nonce.as_deref().map(u64_of).transpose().map_err(h)?.unwrap_or(0),
169
+ chain_id: Some(req.cfg.chain_id),
170
+ ..Default::default()
171
+ };
172
+ let mut cfg = CfgEnv::new_with_spec(spec_of(req.cfg.spec.as_deref()));
173
+ cfg.chain_id = req.cfg.chain_id;
174
+ cfg.disable_nonce_check = req.cfg.skip_nonce;
175
+ cfg.disable_balance_check = req.cfg.skip_balance;
176
+ cfg.disable_block_gas_limit = req.cfg.skip_block_gas_limit;
177
+ cfg.disable_base_fee = req.cfg.no_base_fee;
178
+ cfg.disable_eip3607 = req.cfg.skip_eip3607; // simulations may originate from a contract address
179
+ cfg.limit_contract_code_size = Some(usize::MAX); // allowUnlimitedContractSize, like the JS engine
180
+ cfg.limit_contract_initcode_size = Some(usize::MAX);
181
+
182
+ let db = HostDb { host, accounts: HashMap::new(), storage: HashMap::new() };
183
+ let ctx = Context::mainnet().with_db(db).with_block(block).with_cfg(cfg);
184
+ let mut evm = ctx.build_mainnet_with_inspector(SloadTracer { on: req.cfg.trace_sloads, reads: Vec::new() });
185
+ let res = match evm.inspect_tx(tx) {
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
+ };
193
+ let sloads = evm.inspector.reads.iter().map(|(a, s)| (format!("{a:?}"), format!("{s:#066x}"))).collect();
194
+
195
+ let (success, reason, gas, logs, output, created) = match res.result {
196
+ ExecutionResult::Success { reason, gas, logs, output } => { let created = match &output { Output::Create(_, addr) => addr.map(|a| format!("{a:?}")), _ => None }; (true, format!("{reason:?}"), gas, logs, output.into_data(), created) }
197
+ ExecutionResult::Revert { gas, logs, output } => (false, "revert".to_string(), gas, logs, output, None),
198
+ ExecutionResult::Halt { reason, gas, logs } => (false, format!("{reason:?}"), gas, logs, Bytes::new(), None),
199
+ };
200
+ let mut state = Vec::new();
201
+ for (address, acc) in res.state.iter() {
202
+ if !acc.is_touched() { continue; }
203
+ let deleted = acc.is_selfdestructed() || acc.is_empty();
204
+ let storage = acc.storage.iter().filter(|(_, s)| s.is_changed()).map(|(k, s)| (format!("{k:#066x}"), format!("{:#066x}", s.present_value))).collect();
205
+ let code = if acc.is_created() { acc.info.code.as_ref().map(|c| format!("0x{}", hex::encode(c.original_byte_slice()))) } else { None };
206
+ state.push(AccountOut { address: format!("{address:?}"), deleted, balance: format!("{:#x}", acc.info.balance), nonce: format!("{:#x}", acc.info.nonce), code_hash: format!("{:?}", acc.info.code_hash), code, storage });
207
+ }
208
+ let out = RunResult {
209
+ success, reason, gas_used: gas.tx_gas_used(), gas_refunded: gas.final_refunded(),
210
+ output: format!("0x{}", hex::encode(&output)), created,
211
+ logs: logs.iter().map(|l| LogOut { address: format!("{:?}", l.address), topics: l.data.topics().iter().map(|t| format!("{t:?}")).collect(), data: format!("0x{}", hex::encode(&l.data.data)) }).collect(),
212
+ state, sloads,
213
+ };
214
+ serde_json::to_string(&out).map_err(|e| e.to_string())
215
+ }