vmkit_test.gno
8.94 Kb · 312 lines
1package vmkit
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9// counter is a toy [Machine] used to test the kit itself without dragging in
10// a real guest VM: it writes one byte per step and halts after `total` of
11// them. Anything that works here is a property of the ABI, not of brainfuck.
12type counter struct {
13 total int64
14 done int64
15 trap string
16}
17
18func (c *counter) Step(h Host, fuel int64) (int64, Status) {
19 m := NewMeter(fuel)
20 var used int64
21 for c.done < c.total {
22 if !m.Charge(1) {
23 return used, Running
24 }
25 used++
26 c.done++
27 h.Output([]byte{byte('a' + (c.done-1)%26)})
28 }
29 return used, Halted
30}
31
32func (c *counter) Snapshot() []byte {
33 w := NewWriter(24)
34 w.Int(c.total)
35 w.Int(c.done)
36 w.String(c.trap)
37 return w.Out()
38}
39
40func (c *counter) Restore(b []byte) error {
41 r := NewReader(b)
42 total := r.Int()
43 done := r.Int()
44 trap := r.String()
45 if err := r.Err(); err != nil {
46 return err
47 }
48 c.total, c.done, c.trap = total, done, trap
49 return nil
50}
51
52func (c *counter) Trap() string { return c.trap }
53
54func TestStatusString(t *testing.T) {
55 cases := []struct {
56 s Status
57 want string
58 done bool
59 }{
60 {Running, "running", false},
61 {Halted, "halted", true},
62 {Trapped, "trapped", true},
63 {OutOfFuel, "out of fuel", true},
64 {Status(99), "unknown", true},
65 }
66 for _, tc := range cases {
67 uassert.Equal(t, tc.want, tc.s.String())
68 uassert.Equal(t, tc.done, tc.s.Done())
69 }
70}
71
72func TestMeterCharge(t *testing.T) {
73 m := NewMeter(10)
74 uassert.True(t, m.Charge(4))
75 uassert.Equal(t, int64(4), m.Used())
76 uassert.Equal(t, int64(6), m.Remaining())
77
78 // A charge that does not fit spends nothing, so the caller can stop
79 // before the instruction it cannot pay for.
80 uassert.False(t, m.Charge(7))
81 uassert.Equal(t, int64(4), m.Used())
82
83 uassert.True(t, m.Charge(6))
84 uassert.True(t, m.Exhausted())
85 uassert.False(t, m.Charge(1))
86}
87
88func TestMeterUnmetered(t *testing.T) {
89 m := NewMeter(Unmetered)
90 uassert.True(t, m.Charge(1 << 40))
91 uassert.False(t, m.Exhausted())
92 uassert.Equal(t, Unmetered, m.Remaining())
93}
94
95func TestMeterZeroBudget(t *testing.T) {
96 m := NewMeter(0)
97 uassert.True(t, m.Exhausted())
98 uassert.False(t, m.Charge(1))
99 uassert.Equal(t, int64(0), m.Used())
100}
101
102func TestCodecRoundTrip(t *testing.T) {
103 w := NewWriter(0)
104 w.Byte(0xab)
105 w.Uint32(0xdeadbeef)
106 w.Uint64(0x0102030405060708)
107 w.Int(-1234567)
108 w.Int(0)
109 w.Int(1234567)
110 w.Bytes([]byte{0, 1, 2, 255})
111 w.String("gno.land")
112
113 r := NewReader(w.Out())
114 uassert.Equal(t, uint64(0xab), uint64(r.Byte()))
115 uassert.Equal(t, uint64(0xdeadbeef), uint64(r.Uint32()))
116 uassert.Equal(t, uint64(0x0102030405060708), r.Uint64())
117 uassert.Equal(t, int64(-1234567), r.Int())
118 uassert.Equal(t, int64(0), r.Int())
119 uassert.Equal(t, int64(1234567), r.Int())
120 uassert.Equal(t, 4, len(r.Bytes()))
121 uassert.Equal(t, "gno.land", r.String())
122 uassert.NoError(t, r.Err())
123 uassert.Equal(t, 0, r.Remaining())
124}
125
126func TestCodecIsCanonical(t *testing.T) {
127 // The same state must always produce the same bytes: a snapshot is
128 // consensus state, so two nodes encoding it differently is a fork.
129 build := func() []byte {
130 w := NewWriter(0)
131 w.Int(-7)
132 w.String("x")
133 w.Uint32(9)
134 return w.Out()
135 }
136 uassert.Equal(t, string(build()), string(build()))
137}
138
139func TestCodecTruncated(t *testing.T) {
140 r := NewReader([]byte{1, 2})
141 r.Uint64()
142 uassert.ErrorIs(t, r.Err(), ErrTruncated)
143 // Once latched, every later read is a zero value and the error stands.
144 uassert.Equal(t, "", r.String())
145 uassert.ErrorIs(t, r.Err(), ErrTruncated)
146}
147
148func TestCodecBytesAreCopied(t *testing.T) {
149 w := NewWriter(0)
150 w.Bytes([]byte{1, 2, 3})
151 buf := w.Out()
152 r := NewReader(buf)
153 got := r.Bytes()
154 buf[len(buf)-1] = 99 // mutate the snapshot under the reader
155 uassert.Equal(t, 3, len(got))
156 uassert.Equal(t, uint64(3), uint64(got[2]))
157}
158
159func TestTestHostStorage(t *testing.T) {
160 h := NewTestHost()
161 uassert.Equal(t, 0, len(h.Get([]byte("missing"))))
162
163 h.Set([]byte{0, 1}, []byte("zero-prefixed"))
164 h.Set([]byte("k"), []byte("v"))
165 uassert.Equal(t, "zero-prefixed", string(h.Get([]byte{0, 1})))
166 uassert.Equal(t, "v", string(h.Get([]byte("k"))))
167
168 // Keys are hex so a zero byte survives, and sorted so a test can pin
169 // them.
170 keys := h.Keys()
171 uassert.Equal(t, 2, len(keys))
172 uassert.Equal(t, "0001", keys[0])
173 uassert.Equal(t, "6b", keys[1])
174}
175
176func TestTestHostStorageIsCopied(t *testing.T) {
177 h := NewTestHost()
178 val := []byte("abc")
179 h.Set([]byte("k"), val)
180 val[0] = 'z'
181 uassert.Equal(t, "abc", string(h.Get([]byte("k"))))
182
183 got := h.Get([]byte("k"))
184 got[0] = 'z'
185 uassert.Equal(t, "abc", string(h.Get([]byte("k"))))
186}
187
188func TestTestHostSendNeedsAGrant(t *testing.T) {
189 to := address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
190
191 h := NewTestHost()
192 uassert.ErrorIs(t, h.Send(to, 100), ErrNotGranted)
193 uassert.Equal(t, 0, len(h.Sends()))
194
195 h.Grant(150)
196 uassert.NoError(t, h.Send(to, 100))
197 uassert.Equal(t, 1, len(h.Sends()))
198 uassert.Equal(t, int64(100), h.Sends()[0].Amount)
199
200 // The grant is a budget, not a switch.
201 uassert.ErrorIs(t, h.Send(to, 100), ErrNotGranted)
202 uassert.NoError(t, h.Send(to, 50))
203}
204
205func TestTestHostEventsAndInput(t *testing.T) {
206 h := NewTestHost().WithInput([]byte("hi")).WithHeight(42).WithTime(1700000000)
207 uassert.Equal(t, "hi", string(h.Input()))
208 uassert.Equal(t, int64(42), h.Height())
209 uassert.Equal(t, int64(1700000000), h.Now())
210
211 h.Emit("run", "id", "1", "status", "halted")
212 h.Emit("odd", "dangling")
213 uassert.Equal(t, 2, len(h.Events()))
214 uassert.Equal(t, "run id=1 status=halted", h.Events()[0])
215 uassert.Equal(t, "odd", h.Events()[1]) // odd trailing element dropped
216}
217
218func TestInstanceRunsToCompletion(t *testing.T) {
219 inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
220 h := NewTestHost()
221 uassert.NoError(t, inst.Run(&counter{total: 5}, h, Unmetered))
222 uassert.Equal(t, "halted", inst.Status.String())
223 uassert.Equal(t, int64(5), inst.FuelUsed)
224 uassert.Equal(t, int64(1), inst.Slices)
225 uassert.Equal(t, "abcde", h.OutString())
226}
227
228func TestInstanceResumesAcrossSlices(t *testing.T) {
229 // The property that makes a guest program a contract: five slices of
230 // one unit must equal one slice of five.
231 inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
232 h := NewTestHost()
233 for i := 0; i < 5; i++ {
234 // A realm holds bytes, not a machine: every slice loads a fresh
235 // one from the program and lets Restore carry the state over.
236 uassert.NoError(t, inst.Run(&counter{total: 5}, h, 1))
237 }
238 uassert.Equal(t, "halted", inst.Status.String())
239 uassert.Equal(t, int64(5), inst.FuelUsed)
240 uassert.Equal(t, int64(5), inst.Slices)
241 uassert.Equal(t, "abcde", h.OutString())
242}
243
244func TestInstanceStopsAtItsBudget(t *testing.T) {
245 inst := NewInstance("1", address("g1x"), "counter", []byte("10"), 3)
246 h := NewTestHost()
247 uassert.NoError(t, inst.Run(&counter{total: 10}, h, Unmetered))
248 uassert.Equal(t, "out of fuel", inst.Status.String())
249 uassert.Equal(t, int64(3), inst.FuelUsed)
250 uassert.Equal(t, int64(0), inst.Remaining())
251 uassert.Equal(t, "abc", h.OutString())
252
253 // A second call has nothing to spend and says so instead of looping.
254 uassert.ErrorIs(t, inst.Run(&counter{}, h, Unmetered), ErrBudgetExhausted)
255}
256
257func TestInstanceSliceClamps(t *testing.T) {
258 inst := NewInstance("1", address("g1x"), "counter", nil, 10)
259 inst.FuelUsed = 7
260 uassert.Equal(t, int64(3), inst.Slice(100))
261 uassert.Equal(t, int64(2), inst.Slice(2))
262 uassert.Equal(t, int64(3), inst.Slice(Unmetered))
263
264 open := NewInstance("2", address("g1x"), "counter", nil, Unmetered)
265 uassert.Equal(t, int64(5), open.Slice(5))
266 uassert.Equal(t, Unmetered, open.Slice(Unmetered))
267}
268
269func TestInstanceRejectsABadSnapshot(t *testing.T) {
270 inst := NewInstance("1", address("g1x"), "counter", []byte("5"), Unmetered)
271 inst.Snapshot = []byte{1, 2, 3} // too short for the counter's three fields
272 err := inst.Run(&counter{}, NewTestHost(), Unmetered)
273 uassert.ErrorIs(t, err, ErrTruncated)
274 // The instance is untouched: a snapshot that cannot be decoded costs
275 // gas but never corrupts the stored program.
276 uassert.Equal(t, int64(0), inst.Slices)
277 uassert.Equal(t, "5", string(inst.Program))
278}
279
280func TestStore(t *testing.T) {
281 s := NewStore()
282 uassert.Equal(t, 0, s.Size())
283 uassert.Equal(t, true, s.Get("nope") == nil)
284
285 s.Set(NewInstance("001", address("g1a"), "counter", nil, Unmetered))
286 s.Set(NewInstance("002", address("g1b"), "counter", nil, Unmetered))
287 s.Set(NewInstance("003", address("g1c"), "counter", nil, Unmetered))
288 uassert.Equal(t, 3, s.Size())
289 uassert.Equal(t, "002", s.Get("002").ID)
290
291 ids := ""
292 s.Iterate(func(i *Instance) bool { ids += i.ID + " "; return false })
293 uassert.Equal(t, "001 002 003 ", ids)
294
295 rev := ""
296 s.ReverseIterate(func(i *Instance) bool { rev += i.ID + " "; return false })
297 uassert.Equal(t, "003 002 001 ", rev)
298
299 // Early stop.
300 first := ""
301 s.Iterate(func(i *Instance) bool { first = i.ID; return true })
302 uassert.Equal(t, "001", first)
303
304 uassert.True(t, s.Remove("002"))
305 uassert.False(t, s.Remove("002"))
306 uassert.Equal(t, 2, s.Size())
307}
308
309func TestTrapReason(t *testing.T) {
310 uassert.Equal(t, "boom", TrapReason(&counter{trap: "boom"}))
311 uassert.Equal(t, "", TrapReason(&counter{}))
312}