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

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}