instance.gno
4.76 Kb · 187 lines
1package vmkit
2
3import (
4 "errors"
5
6 "gno.land/p/nt/avl/v0"
7)
8
9// ErrNoInstance is returned when an id names nothing.
10var ErrNoInstance = errors.New("vmkit: no such instance")
11
12// ErrBudgetExhausted is returned when an instance has already spent its total
13// fuel budget, so there is nothing left to step.
14var ErrBudgetExhausted = errors.New("vmkit: instance fuel budget exhausted")
15
16// Instance is one guest program as a realm stores it: the code, the machine
17// state between slices, and the accounting that survives the transaction.
18//
19// It holds a snapshot rather than a [Machine], on purpose. A realm keeps
20// bytes; the machine is rebuilt for the slice that needs it and thrown away
21// after. That is what makes the storage cost of a paused program measurable,
22// which is the whole kill criterion for continuations: if resuming costs more
23// than re-running, continuations are theater.
24type Instance struct {
25 ID string
26 Owner address
27 VM string // which guest machine the Program is for
28
29 Program []byte
30 Snapshot []byte
31
32 Status Status
33 Trap string
34
35 // FuelUsed is the total across every slice run so far.
36 FuelUsed int64
37 // FuelBudget caps FuelUsed across the instance's whole life, or is
38 // [Unmetered] for no cap.
39 FuelBudget int64
40
41 // Slices counts how many transactions have stepped this instance.
42 Slices int64
43
44 Output []byte
45}
46
47// NewInstance returns an instance ready for its first slice.
48func NewInstance(id string, owner address, vm string, program []byte, budget int64) *Instance {
49 if budget < 0 {
50 budget = Unmetered
51 }
52 prog := make([]byte, len(program))
53 copy(prog, program)
54 return &Instance{
55 ID: id,
56 Owner: owner,
57 VM: vm,
58 Program: prog,
59 Status: Running,
60 FuelBudget: budget,
61 }
62}
63
64// Remaining reports the fuel left in the instance's total budget, or
65// [Unmetered].
66func (i *Instance) Remaining() int64 {
67 if i.FuelBudget == Unmetered {
68 return Unmetered
69 }
70 left := i.FuelBudget - i.FuelUsed
71 if left < 0 {
72 return 0
73 }
74 return left
75}
76
77// Slice returns how much fuel the next call to [Instance.Run] may spend when
78// the caller asks for `want`: `want` clamped to what the budget still allows.
79func (i *Instance) Slice(want int64) int64 {
80 left := i.Remaining()
81 if left == Unmetered {
82 return want
83 }
84 if want == Unmetered || want > left {
85 return left
86 }
87 return want
88}
89
90// Run steps m for one slice of at most `fuel` units and folds the result back
91// into the instance: the new snapshot, the status, the fuel spent, and
92// anything the guest wrote to h.
93//
94// m must be a fresh machine loaded from i.Program; Run restores i.Snapshot
95// into it when there is one, so the caller never has to remember the order.
96// The instance is left untouched when Restore fails, which means a snapshot
97// that cannot be decoded costs the caller gas but never corrupts the stored
98// program.
99func (i *Instance) Run(m Machine, h Host, fuel int64) error {
100 if i.Status.Done() && i.Status != Running {
101 return ErrBudgetExhausted
102 }
103 if len(i.Snapshot) > 0 {
104 if err := m.Restore(i.Snapshot); err != nil {
105 return err
106 }
107 }
108 slice := i.Slice(fuel)
109 if slice == 0 {
110 i.Status = OutOfFuel
111 return ErrBudgetExhausted
112 }
113
114 used, status := m.Step(h, slice)
115
116 i.FuelUsed += used
117 i.Slices++
118 i.Snapshot = m.Snapshot()
119 i.Status = status
120 i.Trap = TrapReason(m)
121 if status == Running && i.Remaining() == 0 {
122 i.Status = OutOfFuel
123 }
124 return nil
125}
126
127// Store is the avl-backed set of instances a realm owns, keyed by id.
128type Store struct {
129 tree *avl.Tree
130}
131
132// NewStore returns an empty store.
133func NewStore() *Store { return &Store{tree: avl.NewTree()} }
134
135// Set writes an instance, replacing any instance with the same id.
136func (s *Store) Set(i *Instance) {
137 if i == nil {
138 return
139 }
140 s.tree.Set(i.ID, i)
141}
142
143// Get returns the instance with this id, or nil.
144func (s *Store) Get(id string) *Instance {
145 v := s.tree.Get(id)
146 if v == nil {
147 return nil
148 }
149 inst, ok := v.(*Instance)
150 if !ok {
151 return nil
152 }
153 return inst
154}
155
156// Remove deletes an instance and reports whether it existed.
157func (s *Store) Remove(id string) bool {
158 _, removed := s.tree.Remove(id)
159 return removed
160}
161
162// Size returns how many instances are stored.
163func (s *Store) Size() int { return s.tree.Size() }
164
165// Iterate walks every instance in key order, stopping early when fn returns
166// true.
167func (s *Store) Iterate(fn func(*Instance) bool) {
168 s.tree.Iterate("", "", func(_ string, v any) bool {
169 inst, ok := v.(*Instance)
170 if !ok {
171 return false
172 }
173 return fn(inst)
174 })
175}
176
177// ReverseIterate walks every instance in descending key order, which is how a
178// realm renders newest-first when ids are zero-padded and ascending.
179func (s *Store) ReverseIterate(fn func(*Instance) bool) {
180 s.tree.ReverseIterate("", "", func(_ string, v any) bool {
181 inst, ok := v.(*Instance)
182 if !ok {
183 return false
184 }
185 return fn(inst)
186 })
187}