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

riscvdemo.gno

8.49 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/moul/vendor/nt/ufmt/v2"
 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}