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

bfdemo.gno

5.88 Kb · 197 lines
  1// Package bfdemo is the playground for the guest-VM work: a realm that runs
  2// Brainfuck programs on chain, a slice at a time.
  3//
  4// It is a demo of two libraries and carries no logic of its own:
  5// [p/moul/x/vm/bf](/p/moul/x/vm/bf/v0) is the machine, 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 the thing gno realm code cannot do for itself: a
 10// program that runs out of fuel does not fail, it pauses. The realm keeps the
 11// snapshot, and the next caller pays for the next slice. Upload a program,
 12// call Step a few times, and watch one computation finish across several
 13// transactions.
 14package bfdemo
 15
 16import (
 17	"chain"
 18	"chain/runtime"
 19	"chain/runtime/unsafe"
 20	"time"
 21
 22	"gno.land/p/moul/x/vm/bf/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. Past it,
 33	// an upload has to wait for someone to remove one.
 34	MaxInstances = 64
 35	// MaxOutput caps the bytes one program may write. Past it the guest is
 36	// trapped rather than truncated, so rendered output is never a lie.
 37	MaxOutput = 4096
 38	// MaxInput caps the call input a program can be given.
 39	MaxInput = 1024
 40	// DefaultFuel is the budget an upload gets when it asks for none, and
 41	// the slice size Step uses when asked for none.
 42	DefaultFuel = 100000
 43	// MaxSliceFuel bounds one transaction's work regardless of what the
 44	// caller asked for.
 45	MaxSliceFuel = 5000000
 46)
 47
 48var (
 49	// store holds the instances: program, snapshot, status, accounting.
 50	store = vmkit.NewStore()
 51	// inputs holds each instance's call input, keyed by id. It is separate
 52	// from the instance so vmkit.Instance stays the VM-agnostic record it
 53	// is meant to be.
 54	inputs = avl.NewTree()
 55	idgen  seqid.ID
 56	count  int
 57)
 58
 59// host is the realm-backed [vmkit.Host]. One is built per call, wrapping the
 60// instance being stepped, so a guest's output and authority are scoped to it
 61// and nothing ambient leaks in.
 62type host struct {
 63	inst    *vmkit.Instance
 64	in      []byte
 65	kv      *avl.Tree
 66	overrun bool // the guest wrote past MaxOutput
 67}
 68
 69func (h *host) Caller() address { return h.inst.Owner }
 70func (h *host) Origin() address { return h.inst.Owner }
 71func (h *host) Now() int64      { return time.Now().Unix() }
 72func (h *host) Height() int64   { return runtime.ChainHeight() }
 73func (h *host) Input() []byte   { return h.in }
 74
 75// Get and Set are scoped to the instance by construction: the tree belongs to
 76// the instance being stepped, so one program cannot reach another's storage
 77// even though both live in this one realm. bf never calls them; they are here
 78// because the ABI is the point.
 79func (h *host) Get(key []byte) []byte {
 80	v := h.kv.Get(string(key))
 81	if v == nil {
 82		return nil
 83	}
 84	return v.([]byte)
 85}
 86
 87func (h *host) Set(key, val []byte) { h.kv.Set(string(key), val) }
 88
 89func (h *host) Output(p []byte) {
 90	if len(h.inst.Output)+len(p) > MaxOutput {
 91		h.overrun = true
 92		return
 93	}
 94	h.inst.Output = append(h.inst.Output, p...)
 95}
 96
 97func (h *host) Emit(typ string, kv ...string) { chain.Emit(typ, kv...) }
 98
 99// Send is never granted here. The demo funds no instance, so a guest that
100// tries to move coins is refused. That is the capability rule doing its job,
101// not a missing feature.
102func (h *host) Send(to address, amount int64) error { return vmkit.ErrNotGranted }
103
104func (h *host) Log(msg string) {}
105
106// Upload compiles src and stores it as a new instance, returning its id.
107//
108// Compilation happens here rather than at the first Step, so an unbalanced
109// program is rejected by the transaction that submitted it instead of costing
110// somebody else the gas later.
111func Upload(cur realm, src, input string, budget int64) string {
112	if count >= MaxInstances {
113		panic("bfdemo: too many instances, remove one first")
114	}
115	if len(input) > MaxInput {
116		panic("bfdemo: input too long")
117	}
118	if _, err := bf.CompileDefault(src); err != nil {
119		panic("bfdemo: " + err.Error())
120	}
121	if budget <= 0 {
122		budget = DefaultFuel
123	}
124
125	id := idgen.Next().String()
126	owner := unsafe.PreviousRealm().Address()
127	store.Set(vmkit.NewInstance(id, owner, bf.VMName, []byte(src), budget))
128	if input != "" {
129		inputs.Set(id, input)
130	}
131	count++
132
133	chain.Emit("bf_upload", "id", id, "bytes", ufmt.Sprintf("%d", len(src)))
134	return id
135}
136
137// Step runs one slice of the instance: up to `fuel` guest ops, then stop and
138// keep the snapshot. Anyone may pay for a slice, not only the owner: a paused
139// program that only its owner can advance is a worse demo and no safer, since
140// the program and its budget were both fixed at upload.
141func Step(cur realm, id string, fuel int64) string {
142	inst := store.Get(id)
143	if inst == nil {
144		panic("bfdemo: no such instance")
145	}
146	if inst.Status != vmkit.Running {
147		panic("bfdemo: instance is " + inst.Status.String())
148	}
149	if fuel <= 0 {
150		fuel = DefaultFuel
151	}
152	if fuel > MaxSliceFuel {
153		fuel = MaxSliceFuel
154	}
155
156	prog, err := bf.CompileDefault(string(inst.Program))
157	if err != nil {
158		panic("bfdemo: " + err.Error())
159	}
160	h := &host{inst: inst, in: []byte(inputOf(id)), kv: avl.NewTree()}
161	if err := inst.Run(bf.NewMachine(prog), h, fuel); err != nil {
162		panic("bfdemo: " + err.Error())
163	}
164	if h.overrun {
165		inst.Status = vmkit.Trapped
166		inst.Trap = "output limit reached"
167	}
168
169	chain.Emit("bf_step",
170		"id", id,
171		"status", inst.Status.String(),
172		"fuel", ufmt.Sprintf("%d", inst.FuelUsed),
173	)
174	return inst.Status.String()
175}
176
177// Remove deletes an instance. Owner only.
178func Remove(cur realm, id string) {
179	inst := store.Get(id)
180	if inst == nil {
181		panic("bfdemo: no such instance")
182	}
183	if inst.Owner != unsafe.PreviousRealm().Address() {
184		panic("bfdemo: not your instance")
185	}
186	store.Remove(id)
187	inputs.Remove(id)
188	count--
189}
190
191func inputOf(id string) string {
192	v := inputs.Get(id)
193	if v == nil {
194		return ""
195	}
196	return v.(string)
197}