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}