This folder holds two real contracts transpiled to RandProtocol's RISC-V ISA:
- a Solana SPL Token program;
- an Ethereum ERC-20 contract.
For each one it keeps the input, the generated C, and the deployable image. It also holds
transpile.sh, a script that rebuilds everything from scratch and checks the result
against the chain's interpreters.
Both images are the ones deployed live on RandProtocol chain 13 (v0.4).
RandProtocol runs programs as RV32IM machine code inside its zero-knowledge VM. Every run produces a STARK proof of what the program computed.
A Solana or Ethereum contract is bytecode for another machine, sBPF or EVM. There are two ways to run it on RandProtocol:
| path | how it runs | cost |
|---|---|---|
| interpret | the chain's sBPF or EVM interpreter (itself a RISC-V program) runs the bytecode | every opcode is decoded at run time |
| transpile (this folder) | sbpf2rv / evm2rv turn the bytecode into C once, off-chain; rand-guest compiles that C to RV32IM |
the contract runs natively as RISC-V |
The transpiled program reads the same inputs as the interpreter and publishes the same eight
output words. transpile.sh checks that the outputs match, word for word, on every run.
ERC20.sol ──solc──▶ EVM bytecode ──evm2rv──▶ contract.c ──rand-guest build──▶ image.bin (RV32IM)
spl_token.so (sBPF) ─────────────sbpf2rv──▶ program.c ──rand-guest build──▶ image.bin (RV32IM)
│
rand program deploy image.bin ◀──┘
transpiled/
├── transpile.sh ← the script: build tools, transpile, build, run, compare
├── ethereum/
│ ├── ERC20.sol ← the Solidity source
│ ├── erc20.runtime.hex ← its runtime bytecode (solc 0.8.37, 1 296 bytes)
│ ├── SOLC.md ← how the bytecode was compiled
│ └── out/
│ ├── contract.c ← the transpiled C (evm2rv, stage 2)
│ ├── image.bin ← the RV32IM image to deploy (11 686 words)
│ ├── image.bin.sha256
│ ├── info.txt ← rand-guest info: layout, hc
│ └── run-*.txt ← the image's and the interpreter's outputs per vector
├── solana/
│ ├── spl_token.so ← the SPL Token program (sBPF ELF)
│ ├── SPL_TOKEN.md ← its provenance
│ └── out/
│ ├── program.c ← the transpiled C (sbpf2rv)
│ ├── image.bin ← the RV32IM image to deploy (65 096 words)
│ ├── image.bin.sha256
│ ├── info.txt
│ └── run-transfer*.txt
├── vectors/ ← input words: the interpreters' own test vectors
│ ├── erc20-{transfer,approve,transferFrom}.words
│ └── spl-transfer.{public,input}
└── tools/spl-vector/ ← prints the SPL Token Transfer vector (research's fixture)
- The circuits repo checked out next to this folder, at
../circuits(or setCIRCUITS=<path>). It holdsrand-guest,evm2rv,sbpf2rvand the interpreters. Use tagv0.4(commit7ef3220) to reproduce the numbers below. - Rust 1.98.1:
rustup toolchain install 1.98.1. - clang 23.1.1 with the RISC-V target:
brew install llvmon macOS. The build refuses any other version, because the image digest (hc) depends on the compiler. - To deploy: a RandProtocol wallet (
rand) and a chain whose genesis allows programs this large. Chain 13 allows 65 535 words; chain 12 allowed 4 096.
./transpile.sh # both contracts
./transpile.sh erc20 # Ethereum only
./transpile.sh spl # Solana onlyThe script:
- builds the three tools;
- transpiles each contract;
- builds the image;
- writes the input vectors;
- runs the image and the interpreter on the same inputs;
- exits non-zero if any output word differs.
The generated crates live under ../circuits/*/target/, because rand-guest build needs a
circuits checkout. The outputs are copied into ethereum/out/ and solana/out/.
1. Solidity to EVM runtime bytecode. evm2rv takes runtime bytecode, the code the contract
runs after deployment, not the creation code:
solc --bin-runtime --optimize ERC20.sol # → ethereum/erc20.runtime.hex2. EVM bytecode to C.
../circuits/evm2rv/target/release/evm2rv ethereum/erc20.runtime.hex --out <dir> --chain-id 13Output: 1 296 bytes of bytecode, 74 basic blocks, 51 jump destinations, and contract.c.
- Every EVM opcode becomes C over RandProtocol's 256-bit runtime.
- Stage two, the default, keeps the stack values of each block in C locals.
--chain-idis the valueCHAINIDreturns. It is baked into the program, so the program's digest binds it.
An excerpt of ethereum/out/contract.c:
/* 0x0013 CALLDATASIZE */ u256_from_u32(&r0, evm_calldata_len);
/* 0x0014 LT */ u256_lt(&r0, &r0, &K1);
/* 0x0018 JUMPI */ { int c_ = !u256_is_zero(&r0); evm_sp -= 1; if (c_) goto L_96; }
/* 0x001d SHR */ u256_shr(&r0, &r0, &K2);
/* 0x0024 EQ */ u256_eq(&r1, &K3, &r0); /* K3 = 0x095ea7b3, approve(address,uint256) */3. C to a RISC-V image.
../circuits/rand-guest/target/release/rand-guest build <dir> --max-words 65535
# → image.bin (11686 words, hc a0feae92a7311c9562495717100eb7aea71270a38ed435d06dc31387e0ea8ff6)4. Run it, and compare with the interpreter.
rand-guest run ethereum/out/image.bin --input $(cat vectors/erc20-approve.words)
rand-guest run ../circuits/guests-compiled/bin/evm.bin --input $(cat vectors/erc20-approve.words)Both print the same eight words:
out[0] = 1
out[1] = 942495465
…
out[7] = 696258848
| call | translated cycles | interpreter cycles | outputs |
|---|---|---|---|
transfer(BOB, 250) |
66 235 | 121 638 | equal |
approve(BOB, 5) |
48 119 | 85 645 | equal |
transferFrom(ALICE, BOB, 100) |
88 824 | 161 434 | equal |
The translated contract takes 54–56 % of the interpreter's cycles.
1. The program. solana/spl_token.so is the SPL Token sBPF ELF (see SPL_TOKEN.md).
sbpf2rv takes the compiled ELF directly, so there is no source step.
2. sBPF to C.
../circuits/sbpf2rv/target/release/sbpf2rv solana/spl_token.so --out <dir> --name spl-tokenOutput: 30 functions and 3 546 blocks, as 521 757 bytes of C (program.c).
- Each sBPF function becomes a C function.
- sBPF's 64-bit registers become C locals.
- Memory goes through the runtime, which enforces Solana's memory regions.
- The four
UnknownSyscallwarnings are syscalls the interpreter does not support either. They trap at run time, exactly as the interpreter does.
L_225:
budget -= 2;
r2 = sbpf_ld1(r1, 0); /* the instruction tag */
if (r2 == 3) goto L_336; /* Transfer */
goto L_227;3. C to a RISC-V image.
rand-guest build <dir> --max-words 65535
# → image.bin (65096 words, hc 8ca905ae3c62f503de7de86829040f323e098b53dba5fffe09b42f1aad16758b)4. Run it, and compare with the interpreter. An SPL call takes two inputs:
- public input: the ELF itself, 27 151 words;
- private input: the serialized instruction and its accounts, 10 458 words.
tools/spl-vector/target/release/spl-vector vectors # writes spl-transfer.{public,input}
rand-guest run solana/out/image.bin \
--public $(cat vectors/spl-transfer.public) --input $(cat vectors/spl-transfer.input)
rand-guest run ../circuits/guests-compiled/bin/sbpf.bin \
--public $(cat vectors/spl-transfer.public) --input $(cat vectors/spl-transfer.input)| call | translated cycles | interpreter cycles | outputs |
|---|---|---|---|
Transfer(250) |
765 851 | 694 498 | equal |
About 98 % of an SPL run is the fixed Solana ABI harness: reading the inputs, loading the ELF, and checking memory regions. The harness is the same whether the program is translated or interpreted. The program's own execution runs about 2.4× faster translated, but the translated image also verifies the ELF it is given, which adds roughly 73 000 cycles. So translation pays off for compute-heavy Solana programs, not for short token instructions.
# ERC-20: the image alone
rand --key wallet.key.json program deploy ethereum/out/image.bin
# SPL Token: the image, plus the ELF as the program's public input, fixed at deploy
rand --key wallet.key.json program deploy solana/out/image.bin --public solana/spl_token.so
# call the ERC-20's approve(BOB, 5): the 649 input words stay private; only the proof is published
rand --key wallet.key.json call <program-id> \
$(for w in $(cat vectors/erc20-approve.words); do printf -- '--input %s ' $w; done)Live on chain 13 (2026-09-19):
| program | program id | result |
|---|---|---|
| ERC-20 | f074c4eb834cf01886a8241b6a2e0caf6e1cee5327fee6cb1a1a37436607280d |
deployed at height 381. approve called in tx 279e1f62…, committed at height 919. The receipt's eight outputs equal the interpreter's. The call proof is 3 412 405 bytes, and the fee was 0.00357 RAND |
| SPL Token | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
deployed with its 27 151-word ELF as public input, fee 9.2257 RAND. Calling it is not practical yet: its proof needs about 330 GB of memory |
- Translation is off-chain. The chain sees only the RISC-V image, identified by its digest
hc. - The build is reproducible. Rust 1.98.1, clang 23.1.1 and cc 1.4.6 are pinned, and the environment is scrubbed. Anyone can re-run
transpile.shand must get the samehc. - Each image checks its own source.
- The ERC-20 image hashes the bytecode it is given and refuses any other.
- The SPL image hashes the loaded ELF and refuses any other.
- So the
hcalone identifies the source.
- EVM:
- the nine block-context opcodes (TIMESTAMP, NUMBER, BASEFEE, …) trap;
CALLwith a nonzero value traps;- the expensive precompiles (ecrecover, bn256 pairing, large modexp) exceed today's proof size limit.
- sBPF: cross-program invocation and unknown syscalls trap at run time.
- Proving memory:
- tier 16 (
approve): about 22 GB; - tier 18 (
transfer): about 85 GB; - tier 20 (SPL Token): an estimated 330 GB.
- tier 16 (
The full reference is docs/translators.md in the fullnode repo, and evm2rv/README.md and
sbpf2rv/README.md in the circuits repo.