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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bfdemo.gno

5.92 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/nym-moultest123/x/vm/bf/v0) is the machine, and
  6// [p/moul/x/vm/vmkit](/p/nym-moultest123/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/nym-moultest123/x/vm/bf/v0"
 23	"gno.land/p/nym-moultest123/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}