Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

RV32IM, on chain, a slice at a time

Demo of p/moul/x/vm/riscv (the hart) and p/moul/x/vm/vmkit (the host ABI, the fuel meter, the instance store). This realm holds no logic of its own.

The programs below are flat .text images: the same bytes a cross compiler emits for riscv32im-unknown-none-elf. Nothing here was written in a chain language, and nothing translated it.

A program does not run to completion. It runs until its fuel slice is spent, then pauses: the realm keeps the snapshot and the next caller pays for the next slice. Realm code cannot pause itself, and a guest can.

Samples

Hello, gno.land: Eight instructions and a string constant. The write syscall reads the bytes straight out of the text segment and hands them to Host.Output.

1addi a0, zero, 1          # fd
2lui  a1, 0x1000           # buf
3addi a1, a1, 0x20
4addi a2, zero, 16         # len
5addi a7, zero, 64         # write
6ecall
7addi a7, zero, 93         # exit
8ecall
9.ascii "Hello, gno.land\n"

Upload Hello, gno.land

Sum 1 to 100: A real loop, 303 instructions. Writes nothing: the answer is in t0, and the instance page shows the register file.

1addi s0, zero, 100
2addi t0, zero, 0
3addi t1, zero, 0
4loop:
5addi t1, t1, 1
6add  t0, t0, t1
7bne  t1, s0, loop
8addi a7, zero, 93
9ecall

Upload Sum 1 to 100

Heavy loop: 200,006 instructions, four per iteration. At the largest slice this realm allows it takes three transactions to finish, which is the thing it exists to show.

 1lui  s0, 0xc000           # s0 = 50000
 2addi s0, s0, 0x350
 3addi t0, zero, 0
 4addi t1, zero, 0
 5loop:
 6addi t1, t1, 1
 7add  t0, t0, t1
 8addi t2, zero, 0
 9bne  t1, s0, loop
10addi a7, zero, 93
11ecall

Upload Heavy loop

Multiply and divide: The M extension. t0 = 1000000007 * 3 truncated to 32 bits, then t1 = t0 / 7. Division by zero and the one signed overflow case return values here rather than trapping, which is what the spec says and what a chain needs.

1lui  t0, 0x3b9ad000       # t0 = 1000000007
2addi t0, t0, -1529
3addi t1, zero, 3
4mul  t0, t0, t1
5addi t2, zero, 7
6divu t1, t0, t2
7addi a7, zero, 93
8ecall

Upload Multiply and divide

Compiled by clang: Not written for gno and not written by hand: a freestanding C program, compiled by clang for riscv32im and shipped as the bytes LLVM emitted. It reads the call input, hashes it with FNV-1a and writes eight hex digits back. With the default input it prints fb7ffba0. Source and build command: tools/riscv-guests/fnv.

 1long n = syscall3(SYS_READ, 0, (long)in, sizeof(in));
 2
 3unsigned int h = 2166136261u;
 4for (long i = 0; i < n; i++) {
 5	h ^= in[i];
 6	h *= 16777619u;      /* the reason this is rv32im */
 7}
 8
 9for (int i = 0; i < 8; i++) {
10	unsigned int nib = (h >> (28 - i * 4)) & 0xF;
11	out[i] = nib < 10 ? '0' + nib : 'a' + (nib - 10);
12}
13
14syscall3(SYS_WRITE, 1, (long)out, 8);
15syscall3(SYS_EXIT, 0, 0, 0);

Upload Compiled by clang

A token, in Rust: no_std Rust, compiled by rustc for riscv32im: mint, send and burn, applied from the call input. It links Rust's real core and compiler_builtins, so the balances being 64-bit means a divide runs __udivdi3 here, because RV32 has no 64-bit divide instruction. With the default input it prints alice 70 and bob 80. Source: tools/riscv-guests/ledger.

 1match cmd {
 2	b"send" => {
 3		let (from, rest) = field(rest);
 4		let (to, rest) = field(rest);
 5		let (amount, _) = field(rest);
 6		let n = parse_u32(amount).ok_or("bad amount")?;
 7		let fi = led.find(from).ok_or("unknown sender")?;
 8		if led.balances[fi] < n {
 9			return Err("insufficient balance");
10		}
11		let ti = led.intern(to).ok_or("ledger full or bad name")?;
12		led.balances[fi] -= n;
13		led.balances[ti] += n;
14		Ok(())
15	}
16	_ => Err("unknown command"),
17}

Upload A token, in Rust

Instances

id status instructions slices output
0000003 halted 2229 1 alice 70\\nbob 80\\n
0000002 halted 56 1 ``
0000001 halted 143 1 fb7ffba0

What a slice costs

An RV32IM instruction costs about 8,500 gas here, measured rather than estimated, so a block buys roughly 350,000 guest instructions. Loading the image costs about 19,600 gas per instruction word and is paid again on every resume, which is why the image cap below is small. The numbers and how they were taken are in the library README.

Limits

limit value
instances 64
image bytes 8192
input bytes 1024
output bytes 4096
instructions per slice 100000
address space 1048576