Transaction
0066EA2F428364…E473E4FD01FC
Block 385,159 · index 0 · indexed
Summary
- Hash
- 0066EA2F428364C4D5A0FABF16F06DA9D14A493DF8018EB38761E473E4FD01FC
- Block
- 385,159
- Size
- 32504 bytes
- Gas used
- 42,606,682 / 95,558,800
- Fee
- 286676ugnot
- Status
- success
Messages
- Attached funds
- 13000000ugnot
Arguments · 13
- #1riscvdemo
- #2README.md
- #3# r/moul/x/vm/riscvdemo Demo of [`p/moul/x/vm/riscv`](/p/moul/x/vm/riscv/v0), the RV32IM hart, and [`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0), the host ABI. This realm holds no logic of its own: it stores images, builds a `vmkit.Host`, and renders. What it exists to show is two things realm code cannot do for itself. **The guest was not written for gno.** The programs on the front page are flat `.text` images, the bytes a cross compiler emits for `riscv32im-unknown-none-elf`. `Upload` takes hex, so anything you can build with `clang`, `rustc`, TinyGo or Zig and strip to its text segment goes in unchanged. Two samples are exactly that and neither is a mock up. **Compiled by clang** is a freestanding C program; **A token, in Rust** is `#![no_std]` Rust that mints, sends and burns, linking Rust's real `core` and `compiler_builtins`, so a 64-bit divide in it runs `__udivdi3` on a machine with no 64-bit divide instruction. Both are shipped as the bytes the compiler emitted, and the page shows their source rather than a disassembly, because a disassembly would bury the only interesting fact: nobody wrote the machine code. Sources and build commands: `tools/riscv-guests`. **A program pauses instead of failing.** It runs until its fuel slice is spent, then stops with a snapshot the realm keeps, and the next caller pays for the next slice. The "Heavy loop" sample is 200,006 instructions and takes three transactions at the default slice, which is the whole point. ``` UploadSample("heavy", "", 0) -> id Step(id, 80000) -> "running" Step(id, 80000) -> "running" Step(id, 80000) -> "halted" ``` ## Reading the instance page A guest that writes nothing has still computed something, so the instance page restores the snapshot and shows the register file under its ABI names. That is the only place the state is legible: the hart itself does not survive the transaction, only its snapshot does. ## What a slice costs An RV32IM instruction costs about **8,500 gas**, 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 `MaxImage` is 8 KiB and not a megabyte. The measurements and how they were taken are in the [library README](/p/moul/x/vm/riscv/v0). ## What this realm does not grant `Send` always returns `vmkit.ErrNotGranted`. No instance here is funded, so a guest that tries to move coins is refused, and that is the capability rule working rather than a missing feature. Guest key-value storage is scoped to the instance being stepped by construction: the tree belongs to the instance, so one program cannot reach another's even though both live in this realm. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/x/vm/riscvdemo/v0" gno = "0.9" private = true
- #6render.gno
- #7package riscvdemo import ( "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/x/vm/riscv/v0" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/ufmt/v0" ) // regNames are the ABI names, because nobody reading a register dump thinks in // x-numbers and every compiler's output is annotated with these. var regNames = [32]string{ "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6", } // Render is the realm's gnoweb view. // // - "/" the samples, the instance list, and what the numbers mean. // - "/<id>" one instance: its status, fuel, output and register file. func Render(path string) string { id := strings.TrimSpace(strings.TrimPrefix(path, "/")) if id == "" { return renderHome() } return renderInstance(id) } func renderHome() string { var sb strings.Builder sb.WriteString("# RV32IM, on chain, a slice at a time\n\n") sb.WriteString("Demo of [`p/moul/x/vm/riscv`](/p/moul/x/vm/riscv/v0) (the hart) and ") sb.WriteString("[`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0) (the host ABI, the fuel meter, ") sb.WriteString("the instance store). This realm holds no logic of its own.\n\n") sb.WriteString("The programs below are flat `.text` images: the same bytes a cross compiler ") sb.WriteString("emits for `riscv32im-unknown-none-elf`. Nothing here was written in a ") sb.WriteString("chain language, and nothing translated it.\n\n") sb.WriteString("A program does not run to completion. It runs until its fuel slice is spent, ") sb.WriteString("then pauses: the realm keeps the snapshot and the next caller pays for the ") sb.WriteString("next slice. Realm code cannot pause itself, and a guest can.\n\n") sb.WriteString("## Samples\n\n") for i := range samples { s := &samples[i] sb.WriteString("**" + ui.Inline(s.name) + "**: " + s.note + "\n\n") sb.WriteString("```" + s.lang() + "\n" + s.asm + "\n```\n\n") sb.WriteString(ui.Action("Upload "+s.name, "UploadSample", "name", s.slug, "input", s.input, "budget", ufmt.Sprintf("%d", s.budget)) + "\n\n") } sb.WriteString("## Instances\n\n") t := ui.NewTable("id", "status", "instructions", "slices", "output") store.ReverseIterate(func(i *vmkit.Instance) bool { t.Row( "["+i.ID+"](/r/moul/x/vm/riscvdemo/v0:"+i.ID+")", i.Status.String(), ufmt.Sprintf("%d", i.FuelUsed), ufmt.Sprintf("%d", i.Slices), "`"+ui.Cell(printable(i.Output, 24))+"`", ) return false }) sb.WriteString(t.OrEmpty("No instances yet. Upload one of the samples above.")) sb.WriteString("\n\n") sb.WriteString("## What a slice costs\n\n") sb.WriteString("An RV32IM instruction costs about **8,500 gas** here, measured rather than ") sb.WriteString("estimated, so a block buys roughly **350,000 guest instructions**. Loading ") sb.WriteString("the image costs about **19,600 gas per instruction word** and is paid again ") sb.WriteString("on every resume, which is why the image cap below is small. The numbers and ") sb.WriteString("how they were taken are in the [library README](/p/moul/x/vm/riscv/v0).\n\n") sb.WriteString("## Limits\n\n") lt := ui.NewTable("limit", "value") lt.Row("instances", ufmt.Sprintf("%d", MaxInstances)) lt.Row("image bytes", ufmt.Sprintf("%d", MaxImage)) lt.Row("input bytes", ufmt.Sprintf("%d", MaxInput)) lt.Row("output bytes", ufmt.Sprintf("%d", MaxOutput)) lt.Row("instructions per slice", ufmt.Sprintf("%d", MaxSliceFuel)) lt.Row("address space", ufmt.Sprintf("%d", riscv.MemSize)) sb.WriteString(lt.String()) return sb.String() } func renderInstance(id string) string { var sb strings.Builder sb.WriteString("# Instance " + ui.Inline(id) + "\n\n") sb.WriteString("[← all instances](/r/moul/x/vm/riscvdemo/v0)\n\n") inst := store.Get(id) if inst == nil { sb.WriteString("**No such instance.** It was never uploaded, or its owner removed it.\n") return sb.String() } t := ui.NewTable("field", "value") t.Row("owner", ui.Addr(inst.Owner)) t.Row("vm", ui.Cell(inst.VM)) t.Row("status", inst.Status.String()) if inst.Trap != "" { t.Row("trap", ui.Cell(inst.Trap)) } t.Row("instructions run", ufmt.Sprintf("%d", inst.FuelUsed)) t.Row("budget", fuelText(inst.FuelBudget)) t.Row("slices", ufmt.Sprintf("%d", inst.Slices)) t.Row("image bytes", ufmt.Sprintf("%d", len(inst.Program))) t.Row("image words", ufmt.Sprintf("%d", len(inst.Program)/4)) t.Row("snapshot bytes", ufmt.Sprintf("%d", len(inst.Snapshot))) sb.WriteString(t.String()) sb.WriteString("\n") sb.WriteString("## Registers\n\n") sb.WriteString(renderRegisters(inst)) sb.WriteString("## Output\n\n") if len(inst.Output) == 0 { sb.WriteString(ui.Empty("Nothing written yet.")) } else { sb.WriteString("```\n" + printable(inst.Output, MaxOutput) + "\n```\n") } sb.WriteString("\n") sb.WriteString("## Image\n\n```\n" + hexDump(inst.Program, 64) + "\n```\n\n") if inst.Status == vmkit.Running { sb.WriteString(ui.Action("Run 25000 instructions", "Step", "id", id, "fuel", "25000")) sb.WriteString(" · ") sb.WriteString(ui.Action("Run 100000 instructions", "Step", "id", id, "fuel", "100000")) sb.WriteString("\n") } else { sb.WriteString("This instance is **" + inst.Status.String() + "** and cannot be stepped again.\n") } return sb.String() } // renderRegisters restores the snapshot to read the register file out of it. // The hart is not kept between transactions, only its snapshot is, so this is // the only place the state is legible, and it is the thing worth seeing: a // program that wrote nothing still computed something. func renderRegisters(inst *vmkit.Instance) string { if len(inst.Snapshot) == 0 { return ui.Empty("Not started yet: no snapshot to read.") + "\n\n" } var m riscv.Machine if err := m.Restore(inst.Snapshot); err != nil { return ui.Empty("The snapshot could not be read.") + "\n\n" } reg := m.Registers() var sb strings.Builder t := ui.NewTable("register", "hex", "unsigned", "signed") t.Row("pc", ufmt.Sprintf("0x%08X", m.PC()), "", "") shown := 0 for i := 1; i < 32; i++ { if reg[i] == 0 { continue } t.Row( regNames[i], ufmt.Sprintf("0x%08X", reg[i]), ufmt.Sprintf("%d", reg[i]), ufmt.Sprintf("%d", int64(int32(reg[i]))), ) shown++ } sb.WriteString(t.String()) if shown == 0 { sb.WriteString("\nEvery register except `pc` is zero.\n") } sb.WriteString("\n") return sb.String() } func fuelText(n int64) string { if n == vmkit.Unmetered { return "unmetered" } return ufmt.Sprintf("%d", n) } const hexDigits = "0123456789abcdef" // hexDump shows the image as words, which is how anyone reading RV32 reads it. func hexDump(b []byte, max int) string { truncated := false if len(b) > max { b, truncated = b[:max], true } var sb strings.Builder for i := 0; i+3 < len(b); i += 4 { if i > 0 && i%16 == 0 { sb.WriteString("\n") } else if i > 0 { sb.WriteString(" ") } w := uint32(b[i]) | uint32(b[i+1])<<8 | uint32(b[i+2])<<16 | uint32(b[i+3])<<24 sb.WriteString(ufmt.Sprintf("%08x", w)) } if truncated { sb.WriteString("\n...") } return sb.String() } // printable renders guest output for a code fence: printable ASCII as itself, // everything else as an escape. A guest writes arbitrary bytes, so this is the // only form of it that is safe to show. func printable(b []byte, max int) string { if len(b) == 0 { return "" } truncated := false if len(b) > max { b, truncated = b[:max], true } var sb strings.Builder for _, c := range b { switch { case c == '\n': sb.WriteString("\\n") case c == '\t': sb.WriteString("\\t") case c == '\\': sb.WriteString("\\\\") case c == '`': sb.WriteString("\\x60") case c >= 0x20 && c < 0x7f: sb.WriteByte(c) default: sb.WriteString("\\x") sb.WriteByte(hexDigits[c>>4]) sb.WriteByte(hexDigits[c&0x0f]) } } if truncated { sb.WriteString("…") } return sb.String() }
- #8riscvdemo.gno
- #9// Package riscvdemo runs real compiled machine code on chain, a slice at a // time. // // It is a demo of two libraries and carries no logic of its own: // [p/moul/x/vm/riscv](/p/moul/x/vm/riscv/v0) is the RV32IM hart, and // [p/moul/x/vm/vmkit](/p/moul/x/vm/vmkit/v0) is the host ABI, the fuel meter // and the instance store. // // What it exists to show is that the guest was not written for gno. The // programs on the front page are flat .text images: the same bytes // `rustc --target riscv32im-unknown-none-elf` emits, uploaded as hex. The // realm holds the snapshot between transactions, so one computation finishes // across several of them, and realm code cannot do that for itself. package riscvdemo import ( "chain" "chain/runtime" "chain/runtime/unsafe" "time" "gno.land/p/moul/x/vm/riscv/v0" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/ufmt/v0" ) // Caps. Everything a caller can grow is bounded, because all of it is storage // somebody pays a deposit on. const ( // MaxInstances is how many programs the realm keeps at once. MaxInstances = 64 // MaxImage caps an uploaded text segment at 2,048 instructions. // // This one is not arbitrary. Predecoding costs about 19,600 gas per word // and is redone on every resume, so the image size is a tax on every // transaction that touches the instance, not just the upload. 8 KiB works // out to roughly 40M gas per slice before the guest executes anything, // which is about 1% of a block. MaxImage = 8192 // MaxOutput caps the bytes one program may write. Past it the guest is // trapped rather than truncated, so rendered output is never a lie. MaxOutput = 4096 // MaxInput caps the call input a program can be given. MaxInput = 1024 // DefaultFuel is the budget an upload gets when it asks for none, and the // slice size Step uses when asked for none. DefaultFuel = 100000 // MaxSliceFuel bounds one transaction's work regardless of what the // caller asked for, and it is derived from the measurement rather than // picked: an RV32IM instruction costs about 8,500 gas, so 100,000 of them // is about 850M, a little under a third of a 3G block. Twice this would // still be a legal transaction and a rude one, and it would leave no room // for the predecode and the realm's own storage writes on top. MaxSliceFuel = 100000 ) var ( store = vmkit.NewStore() inputs = avl.NewTree() idgen seqid.ID count int ) // host is the realm-backed [vmkit.Host]. One is built per call, wrapping the // instance being stepped, so a guest's output and authority are scoped to it // and nothing ambient leaks in. type host struct { inst *vmkit.Instance in []byte kv *avl.Tree overrun bool // the guest wrote past MaxOutput } func (h *host) Caller() address { return h.inst.Owner } func (h *host) Origin() address { return h.inst.Owner } func (h *host) Now() int64 { return time.Now().Unix() } func (h *host) Height() int64 { return runtime.ChainHeight() } func (h *host) Input() []byte { return h.in } // Get and Set are scoped to the instance by construction: the tree belongs to // the instance being stepped, so one program cannot reach another's storage // even though both live in this one realm. func (h *host) Get(key []byte) []byte { v := h.kv.Get(string(key)) if v == nil { return nil } return v.([]byte) } func (h *host) Set(key, val []byte) { h.kv.Set(string(key), val) } func (h *host) Output(p []byte) { if len(h.inst.Output)+len(p) > MaxOutput { h.overrun = true return } h.inst.Output = append(h.inst.Output, p...) } func (h *host) Emit(typ string, kv ...string) { chain.Emit(typ, kv...) } // Send is never granted here. The demo funds no instance, so a guest that // tries to move coins is refused. That is the capability rule doing its job, // not a missing feature. func (h *host) Send(to address, amount int64) error { return vmkit.ErrNotGranted } func (h *host) Log(msg string) {} // Upload stores a hex-encoded text image as a new instance and returns its id. // // The image is loaded here rather than at the first Step, so a misaligned or // oversized one is rejected by the transaction that submitted it instead of // costing somebody else the gas later. func Upload(cur realm, hexImage, input string, budget int64) string { img, ok := decodeHex(hexImage) if !ok { panic("riscvdemo: image is not valid hex") } return upload(img, input, budget) } // UploadSample stores one of the programs the front page offers. Writing RV32IM // by hand is not the point of this realm, and without this nobody without a // cross compiler could press a button. func UploadSample(cur realm, name, input string, budget int64) string { s := sampleByName(name) if s == nil { panic("riscvdemo: no such sample") } // A sample carries its own budget because the default is a total, not a // slice: the heavy loop needs 200,006 instructions, and offering a button // that runs out of fuel halfway is a worse demo than no button. if budget <= 0 { budget = s.budget } return upload(s.image(), input, budget) } func upload(img []byte, input string, budget int64) string { if count >= MaxInstances { panic("riscvdemo: too many instances, remove one first") } if len(input) > MaxInput { panic("riscvdemo: input too long") } if len(img) == 0 { panic("riscvdemo: empty image") } if len(img) > MaxImage { panic("riscvdemo: image too large") } // Loading it now is the validation: NewMachine is what rejects an image // that is not a whole number of instructions. if _, err := riscv.NewMachine(img, riscv.DefaultEntry); err != nil { panic("riscvdemo: " + err.Error()) } if budget <= 0 { budget = DefaultFuel } id := idgen.Next().String() owner := unsafe.PreviousRealm().Address() store.Set(vmkit.NewInstance(id, owner, riscv.VMName, img, budget)) if input != "" { inputs.Set(id, input) } count++ chain.Emit("riscv_upload", "id", id, "bytes", ufmt.Sprintf("%d", len(img))) return id } // Step runs one slice of the instance: up to `fuel` guest instructions, then // stop and keep the snapshot. Anyone may pay for a slice, not only the owner: // a paused program that only its owner can advance is a worse demo and no // safer, since the program and its budget were both fixed at upload. func Step(cur realm, id string, fuel int64) string { inst := store.Get(id) if inst == nil { panic("riscvdemo: no such instance") } if inst.Status != vmkit.Running { panic("riscvdemo: instance is " + inst.Status.String()) } if fuel <= 0 { fuel = DefaultFuel } if fuel > MaxSliceFuel { fuel = MaxSliceFuel } m, err := riscv.NewMachine(inst.Program, riscv.DefaultEntry) if err != nil { panic("riscvdemo: " + err.Error()) } h := &host{inst: inst, in: []byte(inputOf(id)), kv: avl.NewTree()} if err := inst.Run(m, h, fuel); err != nil { panic("riscvdemo: " + err.Error()) } if h.overrun { inst.Status = vmkit.Trapped inst.Trap = "output limit reached" } chain.Emit("riscv_step", "id", id, "status", inst.Status.String(), "fuel", ufmt.Sprintf("%d", inst.FuelUsed), ) return inst.Status.String() } // Remove deletes an instance. Owner only. func Remove(cur realm, id string) { inst := store.Get(id) if inst == nil { panic("riscvdemo: no such instance") } if inst.Owner != unsafe.PreviousRealm().Address() { panic("riscvdemo: not your instance") } store.Remove(id) inputs.Remove(id) count-- } func inputOf(id string) string { v := inputs.Get(id) if v == nil { return "" } return v.(string) } // decodeHex accepts the output of any hexdump: an even number of hex digits, // with an optional 0x prefix, and whitespace anywhere. Whitespace is allowed // because that is what a pasted hexdump has in it; anything else is refused, // because a silently mangled image would trap somewhere unrelated later. func decodeHex(s string) ([]byte, bool) { if len(s) >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') { s = s[2:] } digits := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { c := s[i] if c == ' ' || c == '\t' || c == '\n' || c == '\r' { continue } v, ok := hexVal(c) if !ok { return nil, false } digits = append(digits, v) } if len(digits) == 0 || len(digits)%2 != 0 { return nil, false } out := make([]byte, len(digits)/2) for i := 0; i < len(out); i++ { out[i] = digits[i*2]<<4 | digits[i*2+1] } return out, true } func hexVal(c byte) (byte, bool) { switch { case c >= '0' && c <= '9': return c - '0', true case c >= 'a' && c <= 'f': return c - 'a' + 10, true case c >= 'A' && c <= 'F': return c - 'A' + 10, true } return 0, false }
- #10riscvdemo_test.gno
- #11package riscvdemo import ( "strings" "testing" "gno.land/p/moul/x/vm/riscv/v0" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) // resetState puts the realm globals back where init() left them. Realm state // persists for the whole test binary, so every test that asserts on ids or on // the rendered instance list has to start from here. func resetState() { store = vmkit.NewStore() inputs = avl.NewTree() idgen = seqid.ID(0) count = 0 } func alice() address { return testutils.TestAddress("alice") } func TestUploadAndRunToCompletion(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) id := UploadSample(cross(cur), "hello", "", 0) uassert.Equal(t, "halted", Step(cross(cur), id, 0)) inst := store.Get(id) uassert.Equal(t, "Hello, gno.land\n", string(inst.Output)) uassert.Equal(t, int64(1), inst.Slices) uassert.Equal(t, alice(), inst.Owner) uassert.Equal(t, "riscv32im", inst.VM) } // Every sample on the front page has to reach its documented answer, or the // page offers a button that traps when pressed. The words are assembled // outside this repo, so this is the test that catches a mistyped one. func TestEverySampleReachesItsAnswer(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) cases := []struct { slug string reg int want uint32 output string }{ {"hello", 0, 0, "Hello, gno.land\n"}, {"sum100", 5, 5050, ""}, {"heavy", 5, 1250025000, ""}, {"mext", 5, 3000000021, ""}, {"clang", 0, 0, "fb7ffba0"}, {"ledger", 0, 0, "alice 70\nbob 80\n"}, } for _, c := range cases { id := UploadSample(cross(cur), c.slug, sampleByName(c.slug).input, 0) status := "" for i := 0; i < 20; i++ { status = Step(cross(cur), id, 0) if status != "running" { break } } uassert.Equal(t, "halted", status, c.slug+" must halt") inst := store.Get(id) uassert.Equal(t, c.output, string(inst.Output), c.slug+" output") if c.reg != 0 { var m riscv.Machine uassert.NoError(t, m.Restore(inst.Snapshot)) uassert.Equal(t, uint64(c.want), uint64(m.Registers()[c.reg]), c.slug+" result") } } } // TestOneProgramAcrossManyTransactions is what the realm exists to // demonstrate: the same computation, finished over several calls, each paying // for its own slice. func TestOneProgramAcrossManyTransactions(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) id := UploadSample(cross(cur), "heavy", "", 0) status := "" for i := 0; i < 50; i++ { status = Step(cross(cur), id, 80000) if status != "running" { break } } inst := store.Get(id) uassert.Equal(t, "halted", status) uassert.True(t, inst.Slices > 1, "a 200k instruction program must not finish in one slice") uassert.Equal(t, int64(200006), inst.FuelUsed) } func TestUploadTakesHex(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) // addi a7, zero, 93 ; ecall id := Upload(cross(cur), "0x9308d005 73000000", "", 0) _ = id } func TestUploadRejectsBadImages(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) // Called without cross(): a panic raised through a crossing call cannot be // recovered, so uassert would never see it. uassert.PanicsWithMessage(t, cur, "riscvdemo: image is not valid hex", func() { Upload(cur, "zzzz", "", 0) }) uassert.PanicsWithMessage(t, cur, "riscvdemo: image is not valid hex", func() { Upload(cur, "abc", "", 0) }) uassert.PanicsWithMessage(t, cur, "riscvdemo: no such sample", func() { UploadSample(cur, "nope", "", 0) }) uassert.PanicsWithMessage(t, cur, "riscvdemo: riscv: image length is not a whole number of instructions", func() { Upload(cur, "0011", "", 0) }) } func TestDecodeHex(t *testing.T) { b, ok := decodeHex("0xDEADbeef") uassert.True(t, ok) uassert.Equal(t, 4, len(b)) uassert.Equal(t, uint64(0xDE), uint64(b[0])) uassert.Equal(t, uint64(0xEF), uint64(b[3])) _, ok = decodeHex("") uassert.False(t, ok) _, ok = decodeHex("0x") uassert.False(t, ok) // Whitespace is skipped, because a pasted hexdump has it. b, ok = decodeHex("00 11\n22\t33") uassert.True(t, ok) uassert.Equal(t, 4, len(b)) // An odd number of digits is still a refusal, whitespace or not. _, ok = decodeHex("00 1") uassert.False(t, ok) } func TestRenderHomeOffersEverySample(cur realm, t *testing.T) { resetState() out := Render("") uassert.True(t, strings.Contains(out, "RV32IM, on chain"), "the title") for _, s := range samples { uassert.True(t, strings.Contains(out, s.name), "home must offer "+s.slug) } uassert.True(t, strings.Contains(out, "No instances yet"), "the empty state") } // The register file is the interesting half of this VM: a program that writes // nothing still computed something, and the snapshot is the only place it is // legible between transactions. func TestRenderInstanceShowsTheRegisterFile(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(alice())) id := UploadSample(cross(cur), "sum100", "", 0) Step(cross(cur), id, 0) out := Render("/" + id) uassert.True(t, strings.Contains(out, "## Registers"), "the section") uassert.True(t, strings.Contains(out, "5050"), "t0 must show the answer") uassert.True(t, strings.Contains(out, "halted"), "the status") } func TestRenderUnknownInstance(t *testing.T) { uassert.True(t, strings.Contains(Render("/nope"), "No such instance")) } // ExampleRender pins the realm's output for a path whose content cannot drift // with the instance list. The home page and the instance page both produce // consecutive blank lines, which an example block cannot represent, so they // are asserted in the tests above instead. func ExampleRender() { print(Render("/missing")) // Output: // # Instance missing // // [← all instances](/r/moul/x/vm/riscvdemo/v0) // // **No such instance.** It was never uploaded, or its owner removed it. }
- #12samples.gno
- #13package riscvdemo import "gno.land/p/moul/x/vm/riscv/v0" // The programs the front page offers, as the machine words a cross compiler // would have emitted. They are assembled outside this repo and pinned here, // and riscvdemo_test.gno runs every one of them to the expected answer, so a // wrong word is a failing test rather than a page that traps when clicked. type sample struct { slug string // what UploadSample takes: short, no punctuation, stable name string budget int64 // the instance's total instruction budget, not the slice // builtin names an image the library ships compiled, instead of the words // below. Empty means the words are the program. builtin string words []uint32 input string note string asm string } var samples = []sample{ { slug: "hello", name: "Hello, gno.land", words: []uint32{ 0x00100513, 0x000015B7, 0x02058593, 0x01000613, 0x04000893, 0x00000073, 0x05D00893, 0x00000073, 0x6C6C6548, 0x67202C6F, 0x6C2E6F6E, 0x0A646E61, }, budget: 1000, note: "Eight instructions and a string constant. The write syscall reads " + "the bytes straight out of the text segment and hands them to Host.Output.", asm: `addi a0, zero, 1 # fd lui a1, 0x1000 # buf addi a1, a1, 0x20 addi a2, zero, 16 # len addi a7, zero, 64 # write ecall addi a7, zero, 93 # exit ecall .ascii "Hello, gno.land\n"`, }, { slug: "sum100", name: "Sum 1 to 100", words: []uint32{ 0x06400413, 0x00000293, 0x00000313, 0x00130313, 0x006282B3, 0xFE831CE3, 0x05D00893, 0x00000073, }, budget: 1000, note: "A real loop, 303 instructions. Writes nothing: the answer is in t0, " + "and the instance page shows the register file.", asm: `addi s0, zero, 100 addi t0, zero, 0 addi t1, zero, 0 loop: addi t1, t1, 1 add t0, t0, t1 bne t1, s0, loop addi a7, zero, 93 ecall`, }, { slug: "heavy", name: "Heavy loop", words: []uint32{ 0x0000C437, 0x35040413, 0x00000293, 0x00000313, 0x00130313, 0x006282B3, 0x00000393, 0xFE831AE3, 0x05D00893, 0x00000073, }, budget: 250000, note: "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.", asm: `lui s0, 0xc000 # s0 = 50000 addi s0, s0, 0x350 addi t0, zero, 0 addi t1, zero, 0 loop: addi t1, t1, 1 add t0, t0, t1 addi t2, zero, 0 bne t1, s0, loop addi a7, zero, 93 ecall`, }, { slug: "mext", name: "Multiply and divide", words: []uint32{ 0x3B9AD2B7, 0xA0728293, 0x00300313, 0x026282B3, 0x00700393, 0x0272D333, 0x05D00893, 0x00000073, }, budget: 1000, note: "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.", asm: `lui t0, 0x3b9ad000 # t0 = 1000000007 addi t0, t0, -1529 addi t1, zero, 3 mul t0, t0, t1 addi t2, zero, 7 divu t1, t0, t2 addi a7, zero, 93 ecall`, }, { slug: "clang", name: "Compiled by clang", input: "gno.land", budget: 20000, note: "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.", builtin: "fnv", asm: `long n = syscall3(SYS_READ, 0, (long)in, sizeof(in)); unsigned int h = 2166136261u; for (long i = 0; i < n; i++) { h ^= in[i]; h *= 16777619u; /* the reason this is rv32im */ } for (int i = 0; i < 8; i++) { unsigned int nib = (h >> (28 - i * 4)) & 0xF; out[i] = nib < 10 ? '0' + nib : 'a' + (nib - 10); } syscall3(SYS_WRITE, 1, (long)out, 8); syscall3(SYS_EXIT, 0, 0, 0);`, }, { slug: "ledger", name: "A token, in Rust", input: "mint alice 100\nmint bob 50\nsend alice bob 30\n", budget: 20000, note: "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.", builtin: "ledger", asm: `match cmd { b"send" => { let (from, rest) = field(rest); let (to, rest) = field(rest); let (amount, _) = field(rest); let n = parse_u32(amount).ok_or("bad amount")?; let fi = led.find(from).ok_or("unknown sender")?; if led.balances[fi] < n { return Err("insufficient balance"); } let ti = led.intern(to).ok_or("ledger full or bad name")?; led.balances[fi] -= n; led.balances[ti] += n; Ok(()) } _ => Err("unknown command"), }`, }, } // image is what gets uploaded: either the words assembled here, or a program // the library ships because a compiler produced it. func (s *sample) image() []byte { switch s.builtin { case "fnv": return riscv.GuestFNV() case "ledger": return riscv.GuestLedger() } return riscv.Image(s.words) } // lang is the fence tag for what the sample shows. The compiled guest shows // its C, because showing a disassembly of what LLVM did with it would bury the // only interesting fact: nobody wrote the machine code. func (s *sample) lang() string { switch s.builtin { case "fnv": return "c" case "ledger": return "rust" } return "asm" } func sampleByName(slug string) *sample { for i := range samples { if samples[i].slug == slug { return &samples[i] } } return nil }
Result log
msg:0,success:true,log:,events:[]