riscvdemo.gno
8.48 Kb · 278 lines
1// Package riscvdemo runs real compiled machine code on chain, a slice at a
2// time.
3//
4// It is a demo of two libraries and carries no logic of its own:
5// [p/moul/x/vm/riscv](/p/moul/x/vm/riscv/v0) is the RV32IM hart, and
6// [p/moul/x/vm/vmkit](/p/moul/x/vm/vmkit/v0) is the host ABI, the fuel meter
7// and the instance store.
8//
9// What it exists to show is that the guest was not written for gno. The
10// programs on the front page are flat .text images: the same bytes
11// `rustc --target riscv32im-unknown-none-elf` emits, uploaded as hex. The
12// realm holds the snapshot between transactions, so one computation finishes
13// across several of them, and realm code cannot do that for itself.
14package riscvdemo
15
16import (
17 "chain"
18 "chain/runtime"
19 "chain/runtime/unsafe"
20 "time"
21
22 "gno.land/p/moul/x/vm/riscv/v0"
23 "gno.land/p/moul/x/vm/vmkit/v0"
24 "gno.land/p/nt/avl/v0"
25 "gno.land/p/nt/seqid/v0"
26 "gno.land/p/nt/ufmt/v0"
27)
28
29// Caps. Everything a caller can grow is bounded, because all of it is storage
30// somebody pays a deposit on.
31const (
32 // MaxInstances is how many programs the realm keeps at once.
33 MaxInstances = 64
34 // MaxImage caps an uploaded text segment at 2,048 instructions.
35 //
36 // This one is not arbitrary. Predecoding costs about 19,600 gas per word
37 // and is redone on every resume, so the image size is a tax on every
38 // transaction that touches the instance, not just the upload. 8 KiB works
39 // out to roughly 40M gas per slice before the guest executes anything,
40 // which is about 1% of a block.
41 MaxImage = 8192
42 // MaxOutput caps the bytes one program may write. Past it the guest is
43 // trapped rather than truncated, so rendered output is never a lie.
44 MaxOutput = 4096
45 // MaxInput caps the call input a program can be given.
46 MaxInput = 1024
47 // DefaultFuel is the budget an upload gets when it asks for none, and the
48 // slice size Step uses when asked for none.
49 DefaultFuel = 100000
50 // MaxSliceFuel bounds one transaction's work regardless of what the
51 // caller asked for, and it is derived from the measurement rather than
52 // picked: an RV32IM instruction costs about 8,500 gas, so 100,000 of them
53 // is about 850M, a little under a third of a 3G block. Twice this would
54 // still be a legal transaction and a rude one, and it would leave no room
55 // for the predecode and the realm's own storage writes on top.
56 MaxSliceFuel = 100000
57)
58
59var (
60 store = vmkit.NewStore()
61 inputs = avl.NewTree()
62 idgen seqid.ID
63 count int
64)
65
66// host is the realm-backed [vmkit.Host]. One is built per call, wrapping the
67// instance being stepped, so a guest's output and authority are scoped to it
68// and nothing ambient leaks in.
69type host struct {
70 inst *vmkit.Instance
71 in []byte
72 kv *avl.Tree
73 overrun bool // the guest wrote past MaxOutput
74}
75
76func (h *host) Caller() address { return h.inst.Owner }
77func (h *host) Origin() address { return h.inst.Owner }
78func (h *host) Now() int64 { return time.Now().Unix() }
79func (h *host) Height() int64 { return runtime.ChainHeight() }
80func (h *host) Input() []byte { return h.in }
81
82// Get and Set are scoped to the instance by construction: the tree belongs to
83// the instance being stepped, so one program cannot reach another's storage
84// even though both live in this one realm.
85func (h *host) Get(key []byte) []byte {
86 v := h.kv.Get(string(key))
87 if v == nil {
88 return nil
89 }
90 return v.([]byte)
91}
92
93func (h *host) Set(key, val []byte) { h.kv.Set(string(key), val) }
94
95func (h *host) Output(p []byte) {
96 if len(h.inst.Output)+len(p) > MaxOutput {
97 h.overrun = true
98 return
99 }
100 h.inst.Output = append(h.inst.Output, p...)
101}
102
103func (h *host) Emit(typ string, kv ...string) { chain.Emit(typ, kv...) }
104
105// Send is never granted here. The demo funds no instance, so a guest that
106// tries to move coins is refused. That is the capability rule doing its job,
107// not a missing feature.
108func (h *host) Send(to address, amount int64) error { return vmkit.ErrNotGranted }
109
110func (h *host) Log(msg string) {}
111
112// Upload stores a hex-encoded text image as a new instance and returns its id.
113//
114// The image is loaded here rather than at the first Step, so a misaligned or
115// oversized one is rejected by the transaction that submitted it instead of
116// costing somebody else the gas later.
117func Upload(cur realm, hexImage, input string, budget int64) string {
118 img, ok := decodeHex(hexImage)
119 if !ok {
120 panic("riscvdemo: image is not valid hex")
121 }
122 return upload(img, input, budget)
123}
124
125// UploadSample stores one of the programs the front page offers. Writing RV32IM
126// by hand is not the point of this realm, and without this nobody without a
127// cross compiler could press a button.
128func UploadSample(cur realm, name, input string, budget int64) string {
129 s := sampleByName(name)
130 if s == nil {
131 panic("riscvdemo: no such sample")
132 }
133 // A sample carries its own budget because the default is a total, not a
134 // slice: the heavy loop needs 200,006 instructions, and offering a button
135 // that runs out of fuel halfway is a worse demo than no button.
136 if budget <= 0 {
137 budget = s.budget
138 }
139 return upload(s.image(), input, budget)
140}
141
142func upload(img []byte, input string, budget int64) string {
143 if count >= MaxInstances {
144 panic("riscvdemo: too many instances, remove one first")
145 }
146 if len(input) > MaxInput {
147 panic("riscvdemo: input too long")
148 }
149 if len(img) == 0 {
150 panic("riscvdemo: empty image")
151 }
152 if len(img) > MaxImage {
153 panic("riscvdemo: image too large")
154 }
155 // Loading it now is the validation: NewMachine is what rejects an image
156 // that is not a whole number of instructions.
157 if _, err := riscv.NewMachine(img, riscv.DefaultEntry); err != nil {
158 panic("riscvdemo: " + err.Error())
159 }
160 if budget <= 0 {
161 budget = DefaultFuel
162 }
163
164 id := idgen.Next().String()
165 owner := unsafe.PreviousRealm().Address()
166 store.Set(vmkit.NewInstance(id, owner, riscv.VMName, img, budget))
167 if input != "" {
168 inputs.Set(id, input)
169 }
170 count++
171
172 chain.Emit("riscv_upload", "id", id, "bytes", ufmt.Sprintf("%d", len(img)))
173 return id
174}
175
176// Step runs one slice of the instance: up to `fuel` guest instructions, then
177// stop and keep the snapshot. Anyone may pay for a slice, not only the owner:
178// a paused program that only its owner can advance is a worse demo and no
179// safer, since the program and its budget were both fixed at upload.
180func Step(cur realm, id string, fuel int64) string {
181 inst := store.Get(id)
182 if inst == nil {
183 panic("riscvdemo: no such instance")
184 }
185 if inst.Status != vmkit.Running {
186 panic("riscvdemo: instance is " + inst.Status.String())
187 }
188 if fuel <= 0 {
189 fuel = DefaultFuel
190 }
191 if fuel > MaxSliceFuel {
192 fuel = MaxSliceFuel
193 }
194
195 m, err := riscv.NewMachine(inst.Program, riscv.DefaultEntry)
196 if err != nil {
197 panic("riscvdemo: " + err.Error())
198 }
199 h := &host{inst: inst, in: []byte(inputOf(id)), kv: avl.NewTree()}
200 if err := inst.Run(m, h, fuel); err != nil {
201 panic("riscvdemo: " + err.Error())
202 }
203 if h.overrun {
204 inst.Status = vmkit.Trapped
205 inst.Trap = "output limit reached"
206 }
207
208 chain.Emit("riscv_step",
209 "id", id,
210 "status", inst.Status.String(),
211 "fuel", ufmt.Sprintf("%d", inst.FuelUsed),
212 )
213 return inst.Status.String()
214}
215
216// Remove deletes an instance. Owner only.
217func Remove(cur realm, id string) {
218 inst := store.Get(id)
219 if inst == nil {
220 panic("riscvdemo: no such instance")
221 }
222 if inst.Owner != unsafe.PreviousRealm().Address() {
223 panic("riscvdemo: not your instance")
224 }
225 store.Remove(id)
226 inputs.Remove(id)
227 count--
228}
229
230func inputOf(id string) string {
231 v := inputs.Get(id)
232 if v == nil {
233 return ""
234 }
235 return v.(string)
236}
237
238// decodeHex accepts the output of any hexdump: an even number of hex digits,
239// with an optional 0x prefix, and whitespace anywhere. Whitespace is allowed
240// because that is what a pasted hexdump has in it; anything else is refused,
241// because a silently mangled image would trap somewhere unrelated later.
242func decodeHex(s string) ([]byte, bool) {
243 if len(s) >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') {
244 s = s[2:]
245 }
246 digits := make([]byte, 0, len(s))
247 for i := 0; i < len(s); i++ {
248 c := s[i]
249 if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
250 continue
251 }
252 v, ok := hexVal(c)
253 if !ok {
254 return nil, false
255 }
256 digits = append(digits, v)
257 }
258 if len(digits) == 0 || len(digits)%2 != 0 {
259 return nil, false
260 }
261 out := make([]byte, len(digits)/2)
262 for i := 0; i < len(out); i++ {
263 out[i] = digits[i*2]<<4 | digits[i*2+1]
264 }
265 return out, true
266}
267
268func hexVal(c byte) (byte, bool) {
269 switch {
270 case c >= '0' && c <= '9':
271 return c - '0', true
272 case c >= 'a' && c <= 'f':
273 return c - 'a' + 10, true
274 case c >= 'A' && c <= 'F':
275 return c - 'A' + 10, true
276 }
277 return 0, false
278}