An Ethereum VM executed by transformer weights.
Solidity contracts run on a RISC-V CPU that lives in compiled transformer layers: exact results, exact gas, zero training.
Live demo: evmllm.rexstjohn.com | github.com/Verafyai/evmLLM | Sibling project: SVMLLM
EVMLLM runs Ethereum smart contracts on a CPU that lives inside transformer weights. A small EVM interpreter written in C is compiled to RISC-V (RV32IM) and executed by Qwenputer, a stack of transformer layers whose every weight was set by a compiler, not by training: attention heads fetch and decode instructions, feed-forward blocks do the arithmetic, and Gated DeltaNet heads act as exact RAM. The EVM is not hand-built into the weights: it is firmware, and the CPU that runs it is Qwenputer's. Ten Solidity contracts run on it, and every result is checked against py-evm.
On top of the VM sits a judgement filter: after the contract has run, a separate review step looks at the amount, the destination and the history, and returns allow, hold or block with a reason.
Some smart-contract incidents involve code that ran exactly as written, with an outcome a sensible human operator would have stopped. Many others are ordinary bugs or key compromises; this project targets only the first kind. That first kind is what motivated this project: fuse the best of smart contracts (trustless, deterministic and decentralized execution) with the best of human intelligence (judgement based on context).
A transformer that can execute contract bytecode exactly is a step toward a single model that both runs the program deterministically and reasons about whether the result makes sense.
Solidity --solc--> EVM bytecode --+
+--> job block in RAM
calldata, preset storage, gas ----+
C EVM interpreter (firmware/evm.c) --riscv gcc--> RV32IM ELF --> ROM
Qwenputer chip layers (compiled, no training)
+-----------------------------------------------------------+
token ->| attention: fetch pc, registers, ROM words, decode |-> next token
^ | FFN: ALU (add, shift, multiply, compare, branch) | |
| | Gated DeltaNet: exact RAM (EVM stack, memory, storage) | |
| +-----------------------------------------------------------+ |
+------------------------ fed back as the next input -------------------+
- Bytecode. Each demo is compiled with solc 0.8.24 for Shanghai (the bytecode uses PUSH0). The runtime bytecode, calldata, preset storage slots and gas limit are packed into a job block that the firmware reads from RAM.
- Fetch and decode via attention. Qwenputer emits a short record of structured tokens per RV32 instruction (for example
VandF0/F1/F2for value chunks,Jfor jumps,QS/QDfor division quotient digits andBYTEfor output). Attention heads look up the program counter, register values and ROM words from earlier tokens. - ALU in the FFN. Feed-forward blocks implement RV32IM plus Zba arithmetic exactly. The EVM's 256-bit words are 8 x 32-bit limbs: ADD/SUB are carry chains, MUL is schoolbook, DIV/MOD use a single-limb fast path and Knuth algorithm D for multi-limb divisors, EXP is square and multiply.
- Stack, memory and storage as exact RAM. Gated DeltaNet heads with one-hot keys store bytes exactly. The EVM stack (96 entries), memory (2 KB) and storage (48 slots) all live there.
- Result. The firmware prints each executed EVM opcode as two hex characters, then the status, gas, return data and changed storage, all as
BYTEtokens. Every token the model predicts is fed back as its next input.
Across the 23 demo runs, a whole run costs 87 to 194 tokens per executed EVM opcode, fixed overhead included (job load, the JUMPDEST scan and the result print). The smallest run is 16,370 tokens and the largest is 605,944.
| Part | Where it runs |
|---|---|
| RV32IM + Zba CPU (fetch, decode, ALU, registers, RAM) | In the weights (Qwenputer chip layers, compiled by chipformer) |
| EVM interpreter | Software: C firmware executed by that CPU, instruction by instruction |
Bit-exact reference of the circuit (chipformer.rv.ref) |
Python, used to produce the expected token trace |
| Circuit runs in this repo | Qwenputer's fastexec.FreeRun on CPU: the compiled weights in fp32 with sparse matrices, autoregressive |
| Full Qwen3.5-4B merge (18.8 GB) | Not run yet: needs a 24 GB GPU |
| Judgement filter | Ordinary Python/JavaScript after the VM. Not part of the weights |
| Browser VM on the site | JavaScript port of the firmware's EVM, checked against py-evm |
- Execute real solc output: function dispatch, checked arithmetic (Panic 0x11),
require/revertwithError(string), SLOAD/SSTORE, MLOAD/MSTORE, loops and jumps. - Exact gas: execution gas (without the 21,000 intrinsic) matches py-evm, including warm/cold storage access (EIP-2929), SSTORE pricing (EIP-2200/3529 without refunds) and memory expansion.
- Opcodes: STOP, ADD, MUL, SUB, DIV, MOD, EXP, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR, ADDRESS, CALLER, CALLVALUE, CALLDATALOAD, CALLDATASIZE, CALLDATACOPY, CODESIZE, CODECOPY, RETURNDATASIZE, POP, MLOAD, MSTORE, MSTORE8, SLOAD, SSTORE, JUMP, JUMPI, PC, MSIZE, GAS, JUMPDEST, PUSH0 to PUSH32, DUP1 to DUP16, SWAP1 to SWAP16, RETURN, REVERT, INVALID.
All sources are in contracts/Demos.sol. The numbers come from tests/demos_ref.py (firmware on the reference machine, checked against py-evm).
| # | Contract | Input | Result | Gas | EVM ops | Tokens |
|---|---|---|---|---|---|---|
| 1 | SpendCap | $250,000 | returns 250000000000 |
22,581 | 115 | 16,687 |
| 1 | SpendCap | $999,999,999 | returns 999999999000000 |
22,581 | 115 | 16,750 |
| 1 | SpendCap | $1,000,000,001 (over cap) | revert: exceeds $1B cap |
326 | 92 | 16,807 |
| 2 | Counter | add 1 to 41 | returns 42 |
5,477 | 113 | 16,370 |
| 2 | Counter | add 1000 to 41 | returns 1041 |
5,477 | 113 | 16,373 |
| 3 | Fibonacci | fib(10) | returns 55 |
1,637 | 477 | 45,351 |
| 3 | Fibonacci | fib(90) | returns 2880067194370816120 |
12,357 | 3,597 | 313,542 |
| 4 | Factorial | 10! | returns 3628800 |
2,109 | 603 | 57,933 |
| 4 | Factorial | 57! | returns ...000000 |
11,650 | 3,329 | 348,393 |
| 4 | Factorial | 58! (overflow) | revert: Panic(0x11) |
11,699 | 3,342 | 349,759 |
| 5 | GCD | gcd(1071, 462) | returns 21 |
619 | 175 | 19,520 |
| 5 | GCD | gcd(2^128, 6^40) | returns 1099511627776 |
3,269 | 900 | 102,174 |
| 6 | Prime | 97 | returns True |
2,590 | 725 | 69,981 |
| 6 | Prime | 91 | returns False |
1,846 | 516 | 51,345 |
| 6 | Prime | 7919 | returns True |
24,157 | 6,729 | 605,944 |
| 7 | Token | account 0 sends 30 to 1 | returns 70000000000000000000 |
10,924 | 221 | 30,458 |
| 7 | Token | account 1 sends 6 to 0 (short) | revert: insufficient balance |
2,587 | 140 | 23,711 |
| 8 | Vault | withdraw 200 | returns 1800000000000000000000 |
7,706 | 124 | 18,852 |
| 8 | Vault | withdraw 600 (over limit) | revert: over withdrawal limit |
2,427 | 94 | 18,244 |
| 9 | Voting | voter 0 votes for candidate 0 | returns 4 |
10,958 | 224 | 31,929 |
| 9 | Voting | voter 2 votes again (rejected) | revert: already voted |
2,666 | 145 | 25,680 |
| 10 | Escrow | code 424242 | returns 25000000000000000000 |
26,730 | 101 | 16,461 |
| 10 | Escrow | code 111111 (condition not met) | revert: condition not met |
4,564 | 104 | 18,701 |
The flow is transaction -> VM circuits (exact result) -> prompt wrapper -> LLM judge -> allow / hold / block.
- A transaction arrives.
- The VM circuits run the contract exactly and produce an exact result (status, return value, gas, and logs).
- That result is wrapped into the prompt, together with what is known about the sender and the receiver and the run rules.
- The LLM reviews the prompt and answers allow, hold or block, with a reason.
- The decision is final. It can only get stricter than the rule verdict, never looser.
In this repository the VM runs in hand-set weights (compiled, not trained), and the judge prompt output is simulated. No real LLM was called. The single merged model that does both the exact run and the language review is future work, and it has not been tested.
- The VM result is never changed. If the code reverts, the review just reports it. This VM build does not record event logs, so the prompt says
logs: none. - The review (
evmllm/judge.py, ported tojudge.json the site) is ordinary code and not part of the weights. It scores the amount against the account's history (median size), whether the destination was seen before, bursts of withdrawals within 10 minutes, the share of the contract cap and of the balance moved. The rule-based review is a heuristic outside the weights. LLMJudgeis a pluggable interface: pass anycomplete(prompt) -> strfunction that returns JSON{"decision", "reason"}. Examples, the demo site and the tests useSimulatedLLM, which returns canned JSON and is labeledSIMULATED model output, no real LLM was called. An LLM can only make a decision stricter, never looser. With no complete function, the judge abstains and the rules decide.- A parity test checks that the JavaScript prompt and the canned replies match the Python ones for all five scenarios.
Wallet 0xa11c...1ce0 has requested calling this smart contract. Based on everything you know about the context and the run rules, is this allowed?
This is the ordinary token transfer in the scenarios below (placeholder addresses). The canned answer allows it.
Role
You are the transaction reviewer for a wallet. The VM has already run the contract exactly. Decide allow, hold or block based on everything you know about the sender and receiver and the run rules. You may only make the decision stricter, never looser than the rule verdict.
Transaction request
Wallet 0xa11c...1ce0 has requested calling contract 0xc0de...c0de function transfer(3 tokens).
Wallet 0xa11c...1ce0 has requested calling this smart contract. Based on everything you know about the context and the run rules, is this allowed?
VM result (exact)
The VM circuits already ran this contract. This result is exact. It is handed over here, into this prompt.
status: success
return value: returns 97000000000000000000
gas: 10,924
logs: none
Sender and receiver
sender: 0xa11ce00000000000000000000000000000a11ce0
receiver: 0xb0b0000000000000000000000000000000000b0b
contract: Token at 0xc0de00000000000000000000000000000000c0de
Sender history
- transfer(2 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 2 tokens, 30d earlier
- transfer(3 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 3 tokens, 25d earlier
- transfer(2.5 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 2.5 tokens, 20d earlier
- transfer(3 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 3 tokens, 15d earlier
- transfer(4 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 4 tokens, 10d earlier
Receiver history
- transfer(2 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 2 tokens, 30d earlier
- transfer(3 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 3 tokens, 25d earlier
- transfer(2.5 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 2.5 tokens, 20d earlier
- transfer(3 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 3 tokens, 15d earlier
- transfer(4 tokens) from 0xa11ce00000000000000000000000000000a11ce0 to 0xb0b0000000000000000000000000000000000b0b, 4 tokens, 10d earlier
Known destinations: 0xb0b0000000000000000000000000000000000b0b
Balance: 100 tokens
Cap: not recorded
Run rules
Compare the amount with the usual size (the median of this sender's past calls). 100 times the usual size or more adds 45. 20 times or more adds 30. 5 times or more adds 10. No history adds 10.
A destination that has never been seen adds 20.
4 or more withdrawals within 10 minutes adds 25. If that burst would drain over half the balance, add 25 more.
90 percent or more of the contract cap in one call adds 20.
Emptying 90 percent or more of the balance adds 15.
A score of 60 or more blocks. A score of 30 or more holds. Otherwise the rules allow.
If the VM status is not success, the decision is block, because the code itself rejected the call.
The reviewer may only make the decision stricter, never looser than the rule verdict.
Rule verdict: allow (score 0)
Reasons: in line with history, known destination
Reply format
Reply with JSON only, one object: {"decision": "allow" or "hold" or "block", "reason": "one sentence"}
SIMULATED model output, no real LLM was called
{"decision": "allow", "reason": "This sender has transferred about 2 to 4 tokens to this same destination several times. A transfer of 3 tokens matches that history, and the run rules allow it."}
Wallet 0xa11c...1ce0 would like to withdraw 450 ETH. Based on everything you know about the context and the run rules, should that be allowed?
This is the vault withdrawal in the scenarios below. The canned answer blocks it. The reason cites the history: the amount versus the usual size, and a destination that has never been seen.
Role
You are the transaction reviewer for a wallet. The VM has already run the contract exactly. Decide allow, hold or block based on everything you know about the sender and receiver and the run rules. You may only make the decision stricter, never looser than the rule verdict.
Transaction request
Wallet 0xa11c...1ce0 has requested calling contract 0xc0ff...ffee function withdraw(450 ETH).
Wallet 0xa11c...1ce0 would like to withdraw 450 ETH. Based on everything you know about the context and the run rules, should that be allowed?
VM result (exact)
The VM circuits already ran this contract. This result is exact. It is handed over here, into this prompt.
status: success
return value: returns 1550000000000000000000
gas: 7,706
logs: none
Sender and receiver
sender: 0xa11ce00000000000000000000000000000a11ce0
receiver: 0x7e5700000000000000000000000000000000bad0
contract: Vault at 0xc0ffee0000000000000000000000000000c0ffee
Sender history
- withdraw(4 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 4 ETH, 90d earlier
- withdraw(4.5 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 4.5 ETH, 83d earlier
- withdraw(3.8 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 3.8 ETH, 76d earlier
- withdraw(4.2 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 4.2 ETH, 69d earlier
- withdraw(4 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 4 ETH, 62d earlier
- withdraw(4.1 ETH) from 0xa11ce00000000000000000000000000000a11ce0 to 0x9a7b000000000000000000000000000000009a7b, 4.1 ETH, 55d earlier
Receiver history
- none
Known destinations: 0x9a7b000000000000000000000000000000009a7b
Balance: 2,000 ETH
Cap: not recorded
Run rules
Compare the amount with the usual size (the median of this sender's past calls). 100 times the usual size or more adds 45. 20 times or more adds 30. 5 times or more adds 10. No history adds 10.
A destination that has never been seen adds 20.
4 or more withdrawals within 10 minutes adds 25. If that burst would drain over half the balance, add 25 more.
90 percent or more of the contract cap in one call adds 20.
Emptying 90 percent or more of the balance adds 15.
A score of 60 or more blocks. A score of 30 or more holds. Otherwise the rules allow.
If the VM status is not success, the decision is block, because the code itself rejected the call.
The reviewer may only make the decision stricter, never looser than the rule verdict.
Rule verdict: block (score 65)
Reasons: 111x the usual size (median 4.05 ETH); destination 0x7e5700000000000000000000000000000000bad0 never seen before
Reply format
Reply with JSON only, one object: {"decision": "allow" or "hold" or "block", "reason": "one sentence"}
SIMULATED model output, no real LLM was called
{"decision": "block", "reason": "Usual withdrawals from this sender are about 4 ETH to one known address. This call withdraws 450 ETH, about 111 times that size, and the destination has never been seen, so it should be blocked."}
Same prompt shape, still simulated, still no model call:
rapid-drain:{"decision": "block", "reason": "Three withdrawals of 490 ETH already happened in the last few minutes, and this call takes another 490 ETH. Together that is 1,960 ETH and would drain most of the balance, so it should be blocked."}near-cap:{"decision": "hold", "reason": "Past spends were about 35 to 42 million USD. This call spends 950 million USD, about 24 times the usual size and 95 percent of the cap, so it should be held for a person to review."}over-cap:{"decision": "block", "reason": "The VM result is a revert because the call exceeds the cap. The code itself rejected it, so the decision is block."}
Five scenarios with synthetic histories (PYTHONPATH=. python -m evmllm.judge_examples). Every VM run matches py-evm:
| Scenario | Call | VM | Verdict | Reasons |
|---|---|---|---|---|
| Valid withdrawal, 111x usual size, never-seen address | withdraw(450 ETH) |
returns 1550000000000000000000 | BLOCK (65) | 111x the usual size (median 4.05 ETH); destination 0x7e5700000000000000000000000000000000bad0 never seen before |
| Ordinary transfer to a known address | transfer(3 tokens) |
returns 97000000000000000000 | ALLOW (0) | in line with history, known destination |
| Rapid repeated withdrawals, each one within the limit | withdraw(490 ETH) |
returns 40000000000000000000 | BLOCK (65) | 4 withdrawals in 10 minutes (1,960 ETH total); pattern would drain over half the balance; empties 90%+ of 8000 the balance |
| Spend under the $1B cap, but 95% of it in one call | spend($950,000,000) |
returns 950000000000000 | HOLD (50) | 24x the usual size (median 39.0M USD); 95% of the contract cap in one call |
| Spend over the $1B cap: the code itself reverts | spend($1,200,000,000) |
revert: exceeds $1B cap | BLOCK (100) | VM: revert: exceeds $1B cap; the code itself rejected it, nothing to review |
git clone https://github.com/Verafyai/evmLLM && cd evmLLM
# Qwenputer (MIT) at the tested commit, plus a two-line patch so fastexec loads the standalone chip
git clone https://github.com/divagr18/qwenputer /tmp/qwp
git -C /tmp/qwp checkout f07a0b2
git -C /tmp/qwp apply "$PWD/qwenputer-cpu.patch"
sudo apt install gcc-riscv64-unknown-elf # only needed to rebuild the firmware
python3 -m venv .venv && . .venv/bin/activate
python3 -m pip install -r requirements.txt -e /tmp/qwp/experimental/riscv
./firmware/build.sh # C EVM -> firmware/evm.elf (a prebuilt ELF is included)
PYTHONPATH=. python tests/demos_ref.py # 23 demo runs vs py-evm
PYTHONPATH=. python tests/fuzz.py 2000 # 2000 random programs vs py-evm
PYTHONPATH=. python tests/divtest.py # 256-bit DIV/MOD vs Python ints
PYTHONPATH=. python examples/run_on_weights.py # a small program on the compiled weights (about 2 min on CPU)
PYTHONPATH=. python examples/run_on_weights.py --row 10 # gcd(1071, 462) on the weights
THREADS=4 PYTHONPATH=. python -u tests/circuit_run.py 0 3 10 # batched demo rows on the weights
PYTHONPATH=. python -m evmllm.judge_examples # judgement filter scenarios
PYTHONPATH=. python -m evmllm.export_site # rewrite site/data.js from real runs. Do not edit site/data.js by hand.Running on the weights, in short:
from chipformer.rv import core, fastexec, spec as S
from evmllm.host import Job, machine, run_ref
job = Job(code=bytes.fromhex("6007600501805f555f5260205ff3"), trace=True) # 5 + 7, store, return
ref = run_ref(job) # bit-exact reference trace
c = core.build() # RV32IM CPU compiled into weights
fr = fastexec.FreeRun(c, 1, "cpu", False, sparse=True)
core.snapshot(c, machine(job)) # firmware ROM + job into the RAM state
fr.load(0, S.tok("GO"))
out = fr.run(len(ref._toks) - 1)[0] # autoregressive: each output is the next input
assert (out == ref._toks[1:]).all()| Test | Pass | What is compared |
|---|---|---|
| Demo runs (10 contracts, 23 inputs), firmware vs py-evm | 23/23 | status, return data, gas, changed storage |
| Random programs, firmware vs py-evm (Shanghai) | 2000/2000 | status, return data, gas, changed storage |
| 256-bit DIV/MOD, firmware vs Python ints | 400/400 | quotient and remainder, multi-limb divisors |
Browser VM (evm.js) vs py-evm corpus |
2023/2023 | the 23 demos plus the 2000 fuzz cases |
| Judgement filter, JS port vs Python | 5/5 | VM result, verdict, score, reasons |
| Runs on the compiled weights (fastexec, CPU) | 12/12 runs, 338,555 tokens | every generated token vs the reference trace |
Runs on the compiled weights (each generated token fed back as the next input):
| Contract | Input | Tokens | Result |
|---|---|---|---|
| SpendCap | $250,000 | 16,686 | exact match |
| SpendCap | $1,000,000,001 (over cap) | 16,806 | exact match |
| Counter | add 1 to 41 | 16,369 | exact match |
| Fibonacci | fib(10) | 45,350 | exact match |
| Factorial | 10! | 57,932 | exact match |
| GCD | gcd(1071, 462) | 19,519 | exact match |
| Prime | 91 | 51,344 | exact match |
| Token | account 0 sends 30 to 1 | 30,457 | exact match |
| Token | account 1 sends 6 to 0 (short) | 23,710 | exact match |
| Vault | withdraw 600 (over limit) | 18,243 | exact match |
| Voting | voter 2 votes again (rejected) | 25,679 | exact match |
| Escrow | code 424242 | 16,460 | exact match |
firmware/ evm.c (the EVM, 256-bit words as 8 x 32-bit limbs), crt0, linker script, build.sh, evm.elf
evmllm/host.py job packing, reference-machine runner, py-evm runner, result parsing
evmllm/demos.py the 10 contracts and 23 demo inputs
evmllm/judge.py judgement filter (rules, prompt builder, simulated replies; a real model plugs in); judge_examples.py has 5 scenarios
evmllm/export_site.py writes site/data.js from real runs
site/ static demo (index.html, app.js, evm.js, judge.js, style.css, generated data.js)
contracts/ Demos.sol and compiled artifacts (solc 0.8.24)
tests/ demos_ref.py, fuzz.py, divtest.py, circuit_run.py, export_corpus.py, check_js_vm.js
examples/ run_on_weights.py
results/ JSON outputs of the runs above
assets/ logo, mark, favicon, social card, judgement and judge-prompt graphics
qwenputer-cpu.patch two-line fastexec fix for the standalone chip
This is a demo that exact computation can live in a model's weights. It is not a node replacement.
Realistic today
- Sidecar next to the chain software. A node operator runs the model beside their own client and uses it to review, flag or hold transactions in their own RPC endpoint, mempool view or wallet guard. It affects only that operator's infrastructure, not consensus. This is the realistic path today.
- Off-chain checker and audit. Re-run a transaction on the weights machine, compare it to the reference result, and attach the review to an audit trail.
- Judgement layer in front of a wallet or multisig. The exact VM result plus the review step decides allow, hold or block before a signer approves.
Research direction: part of consensus
Making the weights machine part of consensus would need:
- Determinism. GPU floating point can differ across hardware and software versions. The compiled CPU here matched the reference trace token for token on CPU in fp32, but agreement across GPUs and software versions has not been tested, and a full Qwen-sized model is a much bigger problem.
- Cost and speed. A tiny program takes tens of thousands of tokens, and every validator would have to repeat it. Here a whole demo run costs 87 to 194 tokens per executed EVM opcode, and even the smallest run is 16,370 tokens.
- Compute limits. The model run would need to be metered and bounded by gas like any other execution.
The judgement filter is illustrative heuristics on synthetic histories, not a security product.
- The weights runs use Qwenputer's compiled chip layers on CPU (fp32, sparse). The full 18.8 GB merged checkpoint has not been run yet.
- Opcode subset only: no KECCAK256 (so no mappings), no CALL/CREATE/LOG, no SDIV/SMOD/SIGNEXTEND/ADDMOD/MULMOD, no precompiles.
- Build limits: 3,072 bytes of code, 256 bytes of calldata, 2 KB memory, 96-entry stack, 48 storage slots. Going over halts with status "build limit".
- Single call with preset storage. No state trie, no persistence between runs, gas refunds not tracked.
- Slow: the batched CPU runs here averaged under 200 tokens per second (183,929 tokens in 961 s for 9 runs, 154,626 tokens in 876 s for 3 runs), and a demo run is 16,370 to 605,944 tokens.
- The judgement filter is simple heuristics on synthetic histories. It is a design sketch, not a security product.
- Faster execution. Qwenputer's GPU executor and a leaner trace format (skip the per-opcode hex print) should be much faster; this has not been measured yet.
- A chat bridge. A small bridge firmware so the model can call
evm_run(bytecode, calldata)mid-conversation and read back exact results, with the judgement filter as an in-context reviewer. - More of the EVM. KECCAK256 (unlocks Solidity mappings), LOG, SDIV/SMOD/SIGNEXTEND/ADDMOD/MULMOD, larger memory and storage, and multi-call state.
- Smarter review. A real model behind the
LLMJudgeinterface (examples use the simulated replies), richer history features, and per-protocol policies.
Untested, not a claim. Merge the compiled machine into a Qwen-class model, as Qwenputer's merged Qwen3.5-4B checkpoint does for its CPU, so one model does both the language judgement and the exact execution of the EVM firmware. This has not been built or tested here. Its size and speed would be dominated by the base model: Qwenputer's checkpoint is 18.8 GB and needs a 24 GB GPU, while the CPU layers and the EVM firmware are small by comparison.
Every weights run so far uses Qwenputer's compiled chip layers on CPU. The next step is the merged model, where the same CPU sits inside Qwen3.5-4B.
- Get a single 24 GB GPU (the Qwenputer post uses an L4). The merged checkpoint is 18.8 GB, and the post reports about 3,200 CPU tokens per second for its batched executor on an L4.
- Reproduce Qwenputer first, using the commands from the post:
git clone https://github.com/divagr18/qwenputer && cd qwenputer/experimental/riscv && uv sync --extra hf hf download divagr1925/Qwen3.5-4B-Qwenputer --local-dir qwenputer-rv uv run python -m chipformer.rv.chat qwenputer-rv --firmware tests/data/rv/calc-bridge.elf \ --ask "Use calc: what is 123456789 * 987654321?"
- Load
firmware/evm.elfwith a job block in place of the calculator firmware, run the 12 weights rows and the 23 demo inputs, and require every token to match the reference trace, as the CPU runs here do. - Add a small bridge firmware so the model can call
evm_run(bytecode, calldata)mid-conversation, then wire the judgement filter to Qwen itself throughLLMJudge, so one model runs the contract exactly and reviews the result.
Drop me some ETH if you want to support me to keep working on this.
0xF2c09bB10Fd950095DD14Ff3289B82f9E264d826
- Qwenputer by Divyansh Agrawal, @divagr1925 on X: blog post, weights on Hugging Face, code (MIT). EVMLLM would not exist without it: the CPU in the weights, the chipformer compiler, the reference machine and the fastexec executor are all Qwenputer's.
- py-evm (Ethereum Foundation) as the reference EVM, solc for compilation.
- Sibling project: SVMLLM, a Solana sBPF VM in hand-built weights. Code: Verafyai/svmLLM.
MIT, see LICENSE. Qwenputer is MIT, Copyright (c) 2026 Divyansh Agrawal.