Main
/
List
/
Categories
/
Tags
Title card: The Solana VM, written into hand-set neural weights. 0 parameters trained, 34 of 38 syscalls entirely in the weights, 38 of 38 matching Agave in the reference VM, 150 of 150 compiled programs on the weights matching LiteSVM.

The Solana VM, Written Into Hand-Set Neural Network Weights

Six days ago svmLLM was a toy with two 128-byte memory windows. Now it runs real compiled Solana programs, 34 of the 38 syscalls including the cryptography, and matches Agave 4.3.2 on every test. Nothing in it was trained.

17 min read
November 7, 2026

Six days ago I posted svmLLM: a tiny Solana-style virtual machine built out of neural network weights. It had 1,130,672 numbers, none of them trained. It also had two 128-byte memory windows, no syscalls, no accounts and no way to call another program. It was a proof that exact computation can live in weights, and not much more.

So the obvious question was how far it could go. How much of the real Solana VM can you write into weights by hand, and still have it agree with the real thing on every byte?

This post is the answer so far. svmLLM now runs Agave 4.3.2's program runtime, the code that executes every Solana program on mainnet:

  • The VM: SBPF v0, every instruction including ALU32 and byte order, BPF-to-BPF calls with 64 stack frames, and Solana's real memory map.
  • Real programs: compiled with cargo-build-sbf, loaded by Agave's own ELF loader.
  • All 38 syscalls registered on mainnet, from sol_log_ to BLS12-381 pairings.
  • Cross-program invocation through both the C and Rust ABIs, plus the System Program.

Every piece was checked against LiteSVM (which runs Agave 4.3.2's runtime with mainnet's feature set) or Anza's own sBPF interpreter.

And still nothing was trained. Every number in the weights was written by a program.

The short version, and the honest version

The short version people want is "we put 100% of the SVM into AI weights." That isn't quite true, and anyone who builds Solana validators would spot why in about a minute. Here is the accurate picture.

Three columns. In the weights: the VM, memory, 34 of 38 syscalls entirely, and the crypto. Host code in Python, as in Agave: ELF loader, account serializer, CPI bookkeeping, System Program and rent check. Reference only: the BN254 and BLS12-381 pairings. Not built: nonce accounts, other builtins, multi-instruction transactions, SBPF v1 to v3.

In the weights: the entire virtual machine (fetch, decode, the ALU, branches, calls, memory, address translation, faults and the compute meter) and 34 of the 38 syscalls, end to end. That includes SHA-256, Keccak-256, base58, program-derived addresses, Ed25519, Ristretto, secp256k1 recovery, Poseidon, BN254 point arithmetic and compression, and BLS12-381 validation, addition, subtraction, multiplication and decompression.

In host code: the parts Agave itself runs as ordinary host code around the VM. Those are the ELF loader, putting accounts into the VM's input memory and reading them back, the bookkeeping for cross-program calls, and the System Program. These are written in Python to Agave's rules and verified the same way, but they are not weights.

Reference only: the two pairing operations, BN254 (one op of sol_alt_bn128_group_op) and BLS12-381 (sol_curve_pairing_map). They match Agave in the reference VM. In the weights they charge the right compute units, check their pointers, and then stop.

So: the whole VM and 34 of 38 syscalls are in hand-set weights; all 38 match Agave. That's the claim.

Why do this at all

The first post covers the motivation in full. In one line: an AI model that has to reason about a Solana transaction can't actually run it. It guesses. If a model's own weights contain a VM that executes the program exactly, the guessing stops, at least for the part that can be computed.

That only matters if the VM is the real one. A model that runs "something like Solana" is worse than useless, because it will be confidently wrong in exactly the edge cases that matter: the out-of-budget error, the misaligned pointer, the account that silently fails to resize. Those cases are where real money gets lost. So the bar for this work was not "it runs programs." The bar was "it agrees with Agave on every byte, including every failure."

How a VM can be weights

Three ideas from the first post carry all of this.

Numbers are one-hot limbs. A 64-bit value is 16 four-bit pieces, and each piece is a vector of 16 numbers with exactly one 1 in it. A register is a 256-number activation. Every activation is exactly 0 or 1, so float32 arithmetic is exact and nothing drifts.

Any function of one-hot inputs is a two-layer ReLU network. Give the first layer one hidden unit per input combination, weights of 1 and a bias of minus (inputs minus one). Exactly one hidden unit fires. The second layer writes out that combination's answer as a one-hot. A 4-bit adder with carry is a 512-unit layer. Chain 16 of them and you have a 64-bit adder.

Memory is a DeltaNet head. A Gated DeltaNet head with its gate at 1 and its write strength at exactly 0 or 1 overwrites one row of its state per write and reads one row back. That is RAM.

One sBPF instruction as five fixed stages: fetch by attention into program RAM, decode by a constant linear layer, execute with lookup-table ReLU layers, translate addresses with ReLU gates, write back with the delta rule.

One sBPF instruction is one pass through fixed stages: fetch (an exact-match lookup into program memory), decode (a constant matrix from opcode to control gates), execute (lookup-table layers), translate addresses (comparators and gates), and write back (the delta rule). The loop that calls the pass again until the halt flag goes to 1 is Python. So is the sequencing of a syscall's steps, which works the same way: Python reads a 0 or 1 out of the circuits and decides which pass comes next.

What changed since the first post

The real memory map

The toy had two 128-byte windows. Solana programs expect four regions at fixed addresses, so that is what svmLLM has now.

The memory map: program at 0x1_0000_0000, 64 stack frames of 4 KB with gaps at 0x2_0000_0000, a 32 KB heap at 0x3_0000_0000, and the serialized input at 0x4_0000_0000. Memory is a paged DeltaNet RAM keyed by one-hot address limbs.

The program region holds the image Agave's loader produces from a real ELF: text and read-only data, relocated. The stack is 64 frames of 4 KB with gaps between them, and each call moves r10 to the next frame. The heap is 32 KB. The input region holds the serialized accounts and instruction data, laid out exactly as Agave lays them out.

The memory is paged: each byte is a row of DeltaNet state, keyed by the one-hot address limbs. An unwritten row reads as zero. Indexing the row directly gives the same answer as the full matrix multiply, so the machine does that instead of multiplying by billions of zeros.

There are quirks you only find by comparing with the real VM. For example, the stack-gap check looks only at the start address of an access, so an access that runs from a frame into the gap after it is allowed. svmLLM does the same.

ALU32, byte order and calls

The toy had no 32-bit ALU instructions. SBPF v0 has a full set, with a trap in it: add32, sub32 and mul32 sign-extend their 32-bit result into the 64-bit register, and every other 32-bit operation zero-extends. Byte-order instructions (le16 through be64) are nibble permutations, which are just fixed wiring. call and callx push a frame onto a call stack that also lives in DeltaNet RAM. The function table is an exact-match lookup.

Against Anza's interpreter on 600 random programs with calls, stack gaps and every kind of fault, the weights match on 600 of 600: status, return value, compute units, every register and both memory windows.

Syscalls as micro-programs

A Solana program talks to the outside world through syscalls: call with a 32-bit hash of the syscall's name. In svmLLM the hash is matched against the image's syscall table, and then the syscall's micro-program runs.

A syscall in the weights: look up the name hash, charge compute units with a circuit, translate every pointer, compute, write the result and return. A Python generator sequences the steps by reading 0/1 signals from the circuits.

Every micro-program follows Agave's order exactly, because the order is visible: charge the compute units first, then translate each pointer, then compute, then write the result. If the budget check fails, the meter is drained. If a pointer is bad, the error comes out with the exact compute-unit count Agave reports.

The arithmetic is all circuits. Cost formulas (a max, a division by 250, saturating sums) are circuits. SHA-256 and Keccak-f are rotate, shift and xor tables. Base58 is long division by 58. Checking that a log message is valid UTF-8 is a nine-state automaton, written as a table. Formatting a number in hex, decimal or base64 is a table per digit.

Program-derived addresses need one more thing: after hashing the seeds, the address has to be checked to be off the Ed25519 curve. That takes 255-bit field arithmetic.

The cryptography

That on-curve check was the hardest part of the PDA work, and it pointed at the hardest part of the whole project: nine syscalls are elliptic-curve and hash cryptography over 255-bit to 381-bit fields.

The crypto stack from bottom to top: lookup tables, a parallel-prefix wide adder, a batched wide multiplier, Barrett modular reduction, prime fields and Fp2, curve arithmetic, and the syscall micro-program. Measured: about 20 ms per 255-bit field multiply.

The stack, bottom to top:

  • Lookup tables. The same kind of ReLU layer as the VM: a 4-bit add with carry, a 4-bit multiply, and two tiny tables for composing carries.
  • Wide add. A 256-bit add done one limb after another is 64 layers deep. Instead, each limb works out its sum for both possible carry-ins at once, and the carries are combined in log2 steps (a Kogge-Stone parallel-prefix adder, built from those tiny tables).
  • Wide multiply. All 64 x 64 nibble products in one batched table lookup, then the shifted rows summed as a tree of wide adds.
  • Modular reduction. Barrett's method: two more multiplies and at most two subtractions, picked by the borrow bit. It works for any prime, so one implementation covers all five fields.
  • Fields. Prime fields, plus Fp2 for BN254's and BLS12-381's second groups. Inversion and square root are fixed exponentiation chains.
  • Curves. Complete twisted Edwards addition for Ed25519 and Ristretto, which needs no special cases at all. Jacobian coordinates for secp256k1, BN254 and BLS12-381. Subgroup checks wherever the real library does them.

The first version of this was far too slow: about 1.5 seconds per 255-bit multiply, so an Ed25519 scalar multiply would have taken over an hour. The fix came from looking at what a lookup-table layer computes. Its input is exactly one-hot, so exactly one hidden unit fires, and the output is one row of the second weight matrix. Reading that row directly gives the same number as running the whole layer, minus the multiplications by zero. It's the same argument as the paged memory. I checked the two against each other on random inputs for every table. With limbs carried as indices, and Torch kept to one thread, a 255-bit field multiply now takes about 20 ms.

Operation in the weightsMeasured time, one thread
255-bit field multiplyabout 20 ms
381-bit field multiplyabout 30 ms
Ed25519 add / validateabout 10 to 20 s
Ed25519 scalar multiplyabout 100 s
Poseidon, 2 inputsabout 20 s
secp256k1 public key recoveryabout 3 min
BN254 G1 multiplyabout 2 min
BN254 G2 multiply (with subgroup check)about 6 min

The pairings are the exception. A pairing is a Miller loop plus a final exponentiation in a 12-degree extension field, on the order of a hundred thousand field multiplies. On the weights that is modeled at tens of minutes for BN254 and about an hour for BLS12-381. So they are in the reference VM, verified, and not yet in the weights.

Cross-program invocation

A Solana program can call another program, passing along accounts and, if it owns a program-derived address, signing for it. That is cross-program invocation, or CPI, and it's where most of Agave's runtime rules live.

CPI flow: the caller runs on the weights and calls invoke; host code translates the C or Rust structures through the circuit memory port and checks privileges; the callee runs on the weights or as the System Program builtin; host code writes the results back into the caller's memory.

The caller and callee both run on the weights. In between is bookkeeping:

  • reading the instruction and account structures out of the caller's memory, in either the C layout or Rust's (with Rc<RefCell<..>> inside);
  • checking that no account gains signer or writable privileges it didn't have;
  • deriving PDA signers, limiting the call depth to 5, and charging 946 compute units per invoke plus 1 per 250 bytes;
  • after the callee returns, writing each changed account's lamports, owner and data back into the caller's memory.

That bookkeeping is host code here, as it is in Agave, and it reads and writes the caller's memory through the same circuit memory port as everything else.

The System Program is an Agave builtin, so it is host code too. It now handles Transfer, Assign, Allocate, CreateAccount and the four seed-derived variants. Nonce accounts are not built.

How it was checked

None of this is checked against itself. Each test sends the same random inputs to three machines and compares everything they report.

Random cases go to the svmLLM weights, the Python reference VM and LiteSVM or Anza's interpreter; success or error, compute units, log lines and every account's final state are compared byte for byte.

The inputs are deliberately hostile: bad pointers, misaligned structs, overlapping buffers, invalid curve points, non-canonical scalars, compute budgets that run out halfway through a syscall. The outputs compared are the success or the exact error, the compute units consumed, the program's log lines, and every account's lamports, owner and data afterwards.

SuiteCasesWeights vs truthReference vs truth
Random VM programs with calls, gaps, every fault (Anza sbpf 0.12)600600/600600/600
Real compiled programs (LiteSVM)150150/150150/150
Syscall exerciser, non-curve syscalls (LiteSVM)400400/400400/400
CPI transactions: C and Rust ABIs, System Program, rent (LiteSVM)2,500 (16 on the weights)16/162,500/2,500
Curve syscalls, all families including pairings (LiteSVM)2,0002,000/2,000
Curve syscalls end to end on the weights, pairings left out (LiteSVM)2424/2424/24

The curve runs, family by family in the reference: Ed25519, Ristretto and secp256k1 1,500 of 1,500; BN254 800 of 800 (every operation, both byte orders, both success and failure); Poseidon 400 of 400; BLS12-381 500 of 500. Separately, each curve operation in the weights was checked byte for byte against the reference: Ed25519 and Ristretto validate, add, subtract and multiply; secp256k1 recovery, valid and invalid; Poseidon in both byte orders; every BN254 add, multiply and compression op; and BLS12-381 validate, add, subtract, multiply and decompress for both groups.

What the real VM taught me

Most of the work was not writing circuits. It was finding out what Agave actually does, one mismatch at a time. A few of the rules that only came out of comparing against LiteSVM:

  • A VM fault and a syscall error are charged differently. A fault in the VM, like an access violation, drains the whole compute budget. A syscall that fails keeps the exact count it had charged.
  • sol_remaining_compute_units is not registered on mainnet. Calling it is a VM fault, not a syscall.
  • The order of checks is part of the behavior. When an account update breaks two rules at once, the error you get depends on which rule Agave checks first. Each update path has its own fixed order, and getting any of them wrong shows up as the wrong error.
  • Overlapping buffers are an error. If a syscall's output buffer overlaps another buffer, Agave refuses with CopyOverlapping instead of copying.
  • BLS12-381 is enabled on mainnet. I had assumed it wasn't.
  • The BLS pairing has a twist. The value Agave's BLS library (blst) returns is the textbook pairing raised to the power minus three. That falls out of how blst organizes its Miller loop and final exponentiation, and it only showed up by matching one pairing of the two generators against LiteSVM.
  • BN254 decoding has its own rules. It reads flag bits in the top of each point. It checks only the curve, not the subgroup, for G2 addition, but checks both for G2 multiplication and the pairing.
  • A zero-lamport account doesn't exist, but it's still an account. LiteSVM reports it as missing. The runtime treats it as a default account owned by the System Program.
  • SBPF v1, v2 and v3 are not active on mainnet. None of their features is in LiteSVM's mainnet list, so mainnet executes v0 only, and that is what svmLLM implements.

How fast is it

It's slow, and that is not the point, but here are the numbers.

Slowdown versus Anza's interpreter, log scale: ALU about 297,000x, calls 401,000x, stack memory 607,000x, mul 1.3 millionx, div and mod 3.9 millionx.

Measured on one CPU thread against Anza's interpreter on the same machine, the weights run an ALU instruction in about 2.5 ms against 8.5 ns, about 300,000 times slower. Multiply is about 1.3 million times slower and divide about 3.9 million times slower. Stack loads and stores are about 600,000 times slower and calls about 400,000 times.

Those numbers are for the full dense evaluation of the VM's layers. The crypto uses the indexed evaluation described above, and the same trick applies to the VM's own instruction layers. I haven't remeasured the VM with it yet.

How big is it? The shared lookup-table layers and constant tensors come to about 1.03 million numbers, measured by walking every table in the code. Every program adds its own program memory and function table, which are data, not parameters.

What's next

  • Pairings in the weights. It's mostly patience: a pairing is a long but fixed sequence of the field operations that already work.
  • The host code into the weights. Account serialization is fixed-layout copying. CPI privilege checks are comparisons. Both are circuit work of the kind already done here.
  • Nonce accounts, the other builtins, multi-instruction transactions.
  • A real model. svmLLM is still a standalone PyTorch machine. The next step on the first post's list is unchanged: put it inside a pretrained model, so one network does both the exact execution and the judgment about it.

The code, the truth harnesses and every result file are in the svmLLM repository on the full-svm branch. The full results write-up is in docs/FULL_SVM.md.

RSJRex St John
Forward-Looking Statements Notice: Verafy blog articles may contain forward-looking statements regarding Verafy’s future plans, technologies, and market positioning and opinions on possible future achievements. These statements are based on current expectations, predicated on attaining future goals, forecasts, and assumptions that involve risks and uncertainties. Actual outcomes and results may differ materially from those expressed or implied in these statements due to factors such as evolving market conditions, technological challenges, regulatory developments, and other risks beyond Verafy's control. Readers are cautioned not to place undue reliance on forward-looking statements. Verafy undertakes no obligation to update or revise such opinions as a result of later or new information, future events, or otherwise.