← Back to Realms

Realm

gno.land/r/moul/x/vm/bfdemo/v0

Overview

Realm Path
gno.land/r/moul/x/vm/bfdemo/v0
Exported Functions
4
State Entries
11
Source Files
4
Total Package Entries
21

Exported Functions

4 exported functions

State

11 state entries

Source Code

FILES
bfdemo.gno
go
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}
198

Raw Package Data

Raw JSON data