Skip to content

Commit 584dc74

Browse files
committed
Export the rust version to WASM + Use it in a Go library
1 parent 911df60 commit 584dc74

File tree

11 files changed

+497
-1
lines changed

11 files changed

+497
-1
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = ["Maxim Andreev <andreevmaxim@gmail.com>"]
77
license = "MIT"
88
readme = "README.md"
99
repository = "https://github.com/cdump/evmole"
10-
exclude = ["/javascript", "/python", "/benchmark", "/.github"]
10+
exclude = ["/javascript", "/python", "/go", "/benchmark", "/.github"]
1111

1212
[dependencies]
1313
alloy-primitives = { version = "1", default-features = false, features = [
@@ -21,11 +21,13 @@ pyo3 = { version = "0.27", features = ["extension-module"], optional = true }
2121
wasm-bindgen = { version = "0.2", optional = true }
2222
serde-wasm-bindgen = { version = "0.6", optional = true }
2323
serde = { version = "1.0", features = ["derive"], optional = true }
24+
serde_json = { version = "1.0", optional = true }
2425

2526
[features]
2627
serde = ["dep:serde"]
2728
python = ["dep:pyo3"]
2829
javascript = ["dep:wasm-bindgen", "dep:serde-wasm-bindgen", "serde"]
30+
wasm = ["serde", "dep:serde_json"]
2931

3032
# for dev
3133
trace_selectors = []

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.PHONY: wasm
2+
wasm:
3+
cargo build --target wasm32-unknown-unknown --features wasm --release
4+
cp target/wasm32-unknown-unknown/release/evmole.wasm go/evmole.wasm

evmole.wasm

1.44 MB
Binary file not shown.

go/evmole.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package evmole
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"encoding/hex"
7+
"encoding/json"
8+
"fmt"
9+
10+
"github.com/tetratelabs/wazero"
11+
)
12+
13+
//go:embed evmole.wasm
14+
var evmoleWASM []byte
15+
16+
// Selector is a 4-byte function selector
17+
type Selector [4]byte
18+
19+
// String returns the selector as a hex string
20+
func (s Selector) String() string {
21+
return hex.EncodeToString(s[:])
22+
}
23+
24+
// Bytes returns the selector as a byte slice
25+
func (s Selector) Bytes() []byte {
26+
return s[:]
27+
}
28+
29+
// UnmarshalJSON unmarshals a JSON string into a Selector
30+
func (s *Selector) UnmarshalText(data []byte) error {
31+
_, err := hex.Decode(s[:], data)
32+
return err
33+
}
34+
35+
// MarshalText marshals the selector as a hex string
36+
func (s Selector) MarshalText() ([]byte, error) {
37+
return []byte(s.String()), nil
38+
}
39+
40+
// ContractInfoOptions configures what information to extract from contract bytecode
41+
type ContractInfoOptions struct {
42+
// Selectors enables extraction of function selectors
43+
Selectors bool
44+
// Arguments enables extraction of function arguments
45+
Arguments bool
46+
// StateMutability enables extraction of function state mutability
47+
StateMutability bool
48+
// Storage enables extraction of contract storage layout
49+
Storage bool
50+
}
51+
52+
// Function represents a public smart contract function
53+
type Function struct {
54+
// Selector is the 4-byte function selector as hex string (e.g., "aabbccdd")
55+
Selector Selector `json:"selector"`
56+
// BytecodeOffset is the starting byte offset within the EVM bytecode for the function body
57+
BytecodeOffset int `json:"bytecodeOffset"`
58+
// Arguments are the function argument types in canonical format (e.g., "uint256,address[]")
59+
// nil if arguments were not extracted
60+
Arguments *string `json:"arguments,omitempty"`
61+
// StateMutability is the function's state mutability ("pure", "view", "payable", or "nonpayable")
62+
// nil if state mutability was not extracted
63+
StateMutability *string `json:"stateMutability,omitempty"`
64+
}
65+
66+
// StorageRecord represents a storage variable record in a smart contract's storage layout
67+
type StorageRecord struct {
68+
// Slot is the storage slot number as hex string (e.g., "0", "1b")
69+
Slot string `json:"slot"`
70+
// Offset is the byte offset within the storage slot (0-31)
71+
Offset uint8 `json:"offset"`
72+
// Type is the variable type (e.g., "uint256", "mapping(address => uint256)", "bytes32")
73+
Type string `json:"type"`
74+
// Reads is a list of function selectors that read from this storage location
75+
Reads []Selector `json:"reads"`
76+
// Writes is a list of function selectors that write to this storage location
77+
Writes []Selector `json:"writes"`
78+
}
79+
80+
// Contract contains analyzed information about a smart contract
81+
type Contract struct {
82+
// Functions is the list of detected contract functions
83+
// nil if functions were not extracted
84+
Functions []Function `json:"functions,omitempty"`
85+
// Storage is the list of contract storage records
86+
// nil if storage layout was not extracted
87+
Storage []StorageRecord `json:"storage,omitempty"`
88+
}
89+
90+
// ContractInfo extracts information about a smart contract from its EVM bytecode.
91+
//
92+
// Parameters:
93+
// - ctx: Context for cancellation and timeout
94+
// - code: Runtime bytecode as raw bytes
95+
// - options: Optional configuration specifying what data to extract. If nil, extracts selectors only.
96+
//
97+
// Returns:
98+
// - Contract object containing the requested smart contract information
99+
// - Error if extraction fails
100+
//
101+
// Example:
102+
//
103+
// code, _ := hex.DecodeString("6080604052...")
104+
// contract, err := ContractInfo(ctx, code, &ContractInfoOptions{
105+
// Selectors: true,
106+
// StateMutability: true,
107+
// })
108+
func ContractInfo(ctx context.Context, code []byte, options *ContractInfoOptions) (*Contract, error) {
109+
if options == nil {
110+
options = &ContractInfoOptions{Selectors: true}
111+
}
112+
113+
// Create a new WebAssembly runtime
114+
runtime := wazero.NewRuntime(ctx)
115+
defer func() { _ = runtime.Close(ctx) }()
116+
117+
instance, err := runtime.Instantiate(ctx, evmoleWASM)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to instantiate WASM module: %w", err)
120+
}
121+
defer func() { _ = instance.Close(ctx) }()
122+
123+
memory := instance.Memory()
124+
contractInfoFunc := instance.ExportedFunction("contract_info")
125+
if contractInfoFunc == nil {
126+
return nil, fmt.Errorf("could not find exported function: contract_info")
127+
}
128+
129+
// Prepare options byte (bitflags)
130+
var optionsByte uint8
131+
if options.Selectors {
132+
optionsByte |= 1 << 0
133+
}
134+
if options.Arguments {
135+
optionsByte |= 1 << 1
136+
}
137+
if options.StateMutability {
138+
optionsByte |= 1 << 2
139+
}
140+
if options.Storage {
141+
optionsByte |= 1 << 3
142+
}
143+
144+
// Memory layout:
145+
// [0..len(code)]: input code
146+
// [len(code)]: options byte
147+
// [len(code)+1..len(code)+5]: result length (u32)
148+
// [len(code)+5..]: result JSON
149+
codeOffset := uint32(0)
150+
optionsOffset := uint32(len(code))
151+
resultLenOffset := optionsOffset + 1
152+
resultOffset := resultLenOffset + 4
153+
resultCapacity := uint32(1024 * 1024) // 1MB buffer for result
154+
155+
// Write input to memory
156+
if !memory.Write(codeOffset, code) {
157+
return nil, fmt.Errorf("failed to write code to memory")
158+
}
159+
if !memory.Write(optionsOffset, []byte{optionsByte}) {
160+
return nil, fmt.Errorf("failed to write options to memory")
161+
}
162+
163+
// Call the WASM function
164+
results, err := contractInfoFunc.Call(ctx,
165+
uint64(codeOffset), uint64(len(code)),
166+
uint64(optionsOffset),
167+
uint64(resultLenOffset), uint64(resultOffset), uint64(resultCapacity))
168+
if err != nil {
169+
return nil, fmt.Errorf("failed to call contract_info: %w", err)
170+
}
171+
172+
if status := uint32(results[0]); status != 0 {
173+
return nil, fmt.Errorf("contract_info error: status=%d", status)
174+
}
175+
176+
// Read the result length
177+
rawResultLen, ok := memory.Read(resultLenOffset, 4)
178+
if !ok {
179+
return nil, fmt.Errorf("failed to read result length")
180+
}
181+
resultLen := uint32(rawResultLen[0]) | uint32(rawResultLen[1])<<8 |
182+
uint32(rawResultLen[2])<<16 | uint32(rawResultLen[3])<<24
183+
184+
// Read the result JSON
185+
resultJSON, ok := memory.Read(resultOffset, resultLen)
186+
if !ok {
187+
return nil, fmt.Errorf("failed to read result from memory")
188+
}
189+
190+
// Parse JSON result
191+
var contract Contract
192+
if err := json.Unmarshal(resultJSON, &contract); err != nil {
193+
return nil, fmt.Errorf("failed to parse result JSON: %w", err)
194+
}
195+
196+
return &contract, nil
197+
}
198+
199+
func FunctionSelectors(ctx context.Context, code []byte) ([]Selector, error) {
200+
contract, err := ContractInfo(ctx, code, &ContractInfoOptions{Selectors: true})
201+
if err != nil {
202+
return nil, err
203+
}
204+
205+
selectors := make([]Selector, len(contract.Functions))
206+
for i, fn := range contract.Functions {
207+
selectors[i] = fn.Selector
208+
}
209+
return selectors, nil
210+
}

go/evmole.wasm

551 KB
Binary file not shown.

go/evmole_test.go

Lines changed: 163 additions & 0 deletions
Large diffs are not rendered by default.

go/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/cdump/evmole/go
2+
3+
go 1.22.5
4+
5+
require github.com/tetratelabs/wazero v1.7.3

go/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
2+
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=

src/interface_wasm.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! WASM interface for EVMole contract analysis
2+
3+
use crate::ContractInfoArgs;
4+
5+
/// Extracts contract information from EVM bytecode and returns JSON-encoded result.
6+
///
7+
/// # Options Flags (bitfield at `options_ptr`)
8+
///
9+
/// * Bit 0 (0x01): Function selectors
10+
/// * Bit 1 (0x02): Function arguments
11+
/// * Bit 2 (0x04): State mutability
12+
/// * Bit 3 (0x08): Storage layout
13+
/// * Bit 4 (0x10): Disassemble bytecode
14+
/// * Bit 5 (0x20): Basic blocks
15+
///
16+
/// # Returns
17+
///
18+
/// * `0` - Success
19+
/// * `1` - Buffer too small (check `result_len_ptr` for required size)
20+
/// * `2` - Serialization error
21+
#[unsafe(no_mangle)]
22+
pub extern "C" fn contract_info(
23+
code_ptr: *const u8,
24+
code_len: usize,
25+
options_ptr: *const u8,
26+
result_len_ptr: *mut u32,
27+
result_ptr: *mut u8,
28+
result_capacity: usize,
29+
) -> u32 {
30+
let code = unsafe { std::slice::from_raw_parts(code_ptr, code_len) };
31+
let options = unsafe { *options_ptr };
32+
33+
// Parse option flags (bitfield)
34+
let selectors = (options & (1 << 0)) != 0; // Bit 0: selectors
35+
let arguments = (options & (1 << 1)) != 0; // Bit 1: arguments
36+
let state_mutability = (options & (1 << 2)) != 0; // Bit 2: state mutability
37+
let storage = (options & (1 << 3)) != 0; // Bit 3: storage layout
38+
let disassemble = (options & (1 << 4)) != 0; // Bit 4: disassembly
39+
let basic_blocks = (options & (1 << 5)) != 0; // Bit 5: basic blocks
40+
41+
// Build contract info args
42+
let mut args = ContractInfoArgs::new(code);
43+
if selectors {
44+
args = args.with_selectors();
45+
}
46+
if arguments {
47+
args = args.with_arguments();
48+
}
49+
if state_mutability {
50+
args = args.with_state_mutability();
51+
}
52+
if storage {
53+
args = args.with_storage();
54+
}
55+
if disassemble {
56+
args = args.with_disassemble();
57+
}
58+
if basic_blocks {
59+
args = args.with_basic_blocks();
60+
}
61+
62+
// Get contract info
63+
let contract = crate::contract_info(args);
64+
65+
// Serialize to JSON
66+
let json = match serde_json::to_vec(&contract) {
67+
Ok(j) => j,
68+
Err(_) => return 2, // Serialization error
69+
};
70+
71+
let result_len = json.len();
72+
73+
unsafe {
74+
*result_len_ptr = result_len as u32;
75+
}
76+
77+
if result_len > result_capacity {
78+
// Buffer too small - caller should allocate larger buffer and retry
79+
return 1;
80+
}
81+
82+
unsafe {
83+
std::ptr::copy_nonoverlapping(json.as_ptr(), result_ptr, result_len);
84+
}
85+
86+
0 // Success
87+
}

0 commit comments

Comments
 (0)