vesting_test.gno
10.36 Kb · 282 lines
1package vesting
2
3import (
4 "strings"
5 "testing"
6
7 "chain/runtime"
8
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11 "gno.land/p/nt/ufmt/v0"
12)
13
14const (
15 mainnetStart int64 = 1789225200
16 mainnetEnd int64 = 1852383600
17 moul = address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
18)
19
20// TestSeededSurvivesInit is the property the seed exists for: this realm is
21// private = true, so a redeploy re-runs init() and wipes everything else. The
22// one schedule the page is about has to come back without a transaction.
23func TestSeededSurvivesInit(t *testing.T) {
24 o, s, e, delayed, ok := ScheduleOf(moul)
25 uassert.True(t, ok, "moul's schedule is missing after init")
26 uassert.Equal(t, int64(106560000000), o)
27 uassert.Equal(t, mainnetStart, s)
28 uassert.Equal(t, mainnetEnd, e)
29 uassert.False(t, delayed)
30}
31
32// TestDeclareWritesOnlyYourOwnRow is the entire trust model. A caller cannot
33// name an address, so it cannot describe one but its own.
34func TestDeclareWritesOnlyYourOwnRow(cur realm, t *testing.T) {
35 alice := testutils.TestAddress("alice")
36 bob := testutils.TestAddress("bob")
37
38 testing.SetRealm(testing.NewUserRealm(alice))
39 Declare(cross(cur), 500, 100, 200, false)
40
41 o, s, e, delayed, ok := ScheduleOf(alice)
42 uassert.True(t, ok)
43 uassert.Equal(t, int64(500), o)
44 uassert.Equal(t, int64(100), s)
45 uassert.Equal(t, int64(200), e)
46 uassert.False(t, delayed)
47
48 // Nothing alice did touched bob.
49 _, _, _, _, ok = ScheduleOf(bob)
50 uassert.False(t, ok, "declaring for alice wrote a row for bob")
51}
52
53func TestDeclareReplacesAndForgetRemoves(cur realm, t *testing.T) {
54 carol := testutils.TestAddress("carol")
55
56 testing.SetRealm(testing.NewUserRealm(carol))
57 Declare(cross(cur), 500, 100, 200, false)
58 testing.SetRealm(testing.NewUserRealm(carol))
59 Declare(cross(cur), 900, 10, 20, true)
60
61 o, s, e, delayed, ok := ScheduleOf(carol)
62 uassert.True(t, ok)
63 uassert.Equal(t, int64(900), o)
64 uassert.Equal(t, int64(10), s)
65 uassert.Equal(t, int64(20), e)
66 uassert.True(t, delayed, "the delayed flag did not survive")
67
68 testing.SetRealm(testing.NewUserRealm(carol))
69 Forget(cross(cur))
70 _, _, _, _, ok = ScheduleOf(carol)
71 uassert.False(t, ok, "Forget left the row behind")
72}
73
74func TestForgetWithoutADeclarationAborts(cur realm, t *testing.T) {
75 dave := testutils.TestAddress("dave")
76 testing.SetRealm(testing.NewUserRealm(dave))
77 uassert.AbortsContains(t, cur, "nothing declared", func() { Forget(cross(cur)) })
78}
79
80// TestSeededRowIsNotWritable: the seeds are source, so a signer who happens to
81// hold a seeded address still cannot rewrite it from a transaction. Otherwise
82// a redeploy would silently revert whatever they set.
83func TestSeededRowIsNotWritable(cur realm, t *testing.T) {
84 testing.SetRealm(testing.NewUserRealm(moul))
85 uassert.AbortsContains(t, cur, "seeded in the source", func() {
86 Declare(cross(cur), 1, 2, 3, false)
87 })
88 testing.SetRealm(testing.NewUserRealm(moul))
89 uassert.AbortsContains(t, cur, "seeded in the source", func() { Forget(cross(cur)) })
90}
91
92func TestDeclareRejectsAnImpossibleSchedule(cur realm, t *testing.T) {
93 erin := testutils.TestAddress("erin")
94 testing.SetRealm(testing.NewUserRealm(erin))
95 uassert.AbortsContains(t, cur, "start time must be before end time", func() {
96 Declare(cross(cur), 100, 200, 100, false)
97 })
98 testing.SetRealm(testing.NewUserRealm(erin))
99 uassert.AbortsContains(t, cur, "cannot be negative", func() {
100 Declare(cross(cur), -1, 0, 100, false)
101 })
102}
103
104// TestDeclareZeroMeansNoSchedule lets an address say "nothing of mine is
105// locked", which is a real and useful claim.
106func TestDeclareZeroMeansNoSchedule(cur realm, t *testing.T) {
107 frank := testutils.TestAddress("frank")
108 testing.SetRealm(testing.NewUserRealm(frank))
109 Declare(cross(cur), 0, 0, 0, false)
110
111 o, _, _, _, ok := ScheduleOf(frank)
112 uassert.True(t, ok)
113 uassert.Equal(t, int64(0), o)
114
115 spend, known := SpendableOf(frank)
116 uassert.True(t, known)
117 uassert.Equal(t, balanceOf(frank), spend)
118}
119
120// TestUnknownAddressReportsItsBalanceAndSaysSo: the balance is real, the
121// spendable figure is not, and the caller is told which is which.
122func TestUnknownAddressReportsItsBalanceAndSaysSo(t *testing.T) {
123 stranger := testutils.TestAddress("stranger")
124
125 spend, ok := SpendableOf(stranger)
126 uassert.False(t, ok, "an unknown address must not claim a known schedule")
127 uassert.Equal(t, balanceOf(stranger), spend)
128
129 locked, ok := LockedOf(stranger)
130 uassert.False(t, ok)
131 uassert.Equal(t, int64(0), locked)
132}
133
134func TestCountTracksTheRegistry(cur realm, t *testing.T) {
135 before := Count()
136 grace := testutils.TestAddress("grace")
137 testing.SetRealm(testing.NewUserRealm(grace))
138 Declare(cross(cur), 10, 1, 2, false)
139 uassert.Equal(t, before+1, Count())
140 testing.SetRealm(testing.NewUserRealm(grace))
141 Forget(cross(cur))
142 uassert.Equal(t, before, Count())
143}
144
145// TestLockedIsCappedByTheBalance: an account that spent while its coins were
146// free can owe the schedule more than it now holds. The honest answer is that
147// nothing moves, never a negative spendable.
148func TestLockedIsCappedByTheBalance(cur realm, t *testing.T) {
149 poor := testutils.TestAddress("poor")
150 testing.SetRealm(testing.NewUserRealm(poor))
151 Declare(cross(cur), 1000000000000, mainnetStart, mainnetEnd, false)
152
153 locked, ok := LockedOf(poor)
154 uassert.True(t, ok)
155 uassert.Equal(t, balanceOf(poor), locked, "locked must not exceed the balance")
156
157 spend, _ := SpendableOf(poor)
158 uassert.Equal(t, int64(0), spend)
159 uassert.True(t, spend >= 0, "spendable went negative")
160}
161
162func TestIsAddr(t *testing.T) {
163 uassert.True(t, isAddr("g1manfred47kzduec920z88wfr64ylksmdcedlf5"))
164 uassert.False(t, isAddr(""))
165 uassert.False(t, isAddr("g1manfred"))
166 uassert.False(t, isAddr("x1manfred47kzduec920z88wfr64ylksmdcedlf5"))
167 uassert.False(t, isAddr("g1MANFRED47kzduec920z88wfr64ylksmdcedlf5"))
168 uassert.False(t, isAddr("g1manfred47kzduec920z88wfr64ylksmdcedlf5x"))
169}
170
171func TestGnotFormatting(t *testing.T) {
172 uassert.Equal(t, "0.000000 GNOT", gnot(0))
173 uassert.Equal(t, "0.000001 GNOT", gnot(1))
174 uassert.Equal(t, "1.000000 GNOT", gnot(1000000))
175 uassert.Equal(t, "106560.000000 GNOT", gnot(106560000000))
176 uassert.Equal(t, "1.234567 GNOT", gnot(1234567))
177 uassert.Equal(t, "-1.500000 GNOT", gnot(-1500000))
178}
179
180func TestPermilleFormatting(t *testing.T) {
181 uassert.Equal(t, "0.0%", permille(0))
182 uassert.Equal(t, "5.0%", permille(50))
183 uassert.Equal(t, "49.9%", permille(499))
184 uassert.Equal(t, "100.0%", permille(1000))
185}
186
187func TestDurationFormatting(t *testing.T) {
188 uassert.Equal(t, "complete", duration(0))
189 uassert.Equal(t, "complete", duration(-1))
190 uassert.Equal(t, "0h", duration(60))
191 uassert.Equal(t, "1h", duration(3600))
192 uassert.Equal(t, "1d 0h", duration(86400))
193 uassert.Equal(t, "2d 3h", duration(2*86400+3*3600))
194}
195
196// TestCivilDates pins the date arithmetic against dates that are known exactly,
197// including the two ends of the real mainnet term and a leap day.
198func TestCivilDates(t *testing.T) {
199 uassert.Equal(t, "1970-01-01", tstamp(0))
200 uassert.Equal(t, "2000-02-29", tstamp(951782400))
201 uassert.Equal(t, "2026-09-12", tstamp(mainnetStart))
202 uassert.Equal(t, "2028-09-12", tstamp(mainnetEnd))
203}
204
205// TestRenderShapes walks the three routes the page serves and checks each one
206// names its source, which is the promise the whole realm makes.
207func TestRenderShapes(t *testing.T) {
208 index := Render("")
209 uassert.True(t, strings.Contains(index, "# Vesting"), "index has no title")
210 uassert.True(t, strings.Contains(index, "no realm can read one"),
211 "index does not say the schedule is supplied")
212
213 seeded := Render(moul.String())
214 uassert.True(t, strings.Contains(seeded, "seeded in this realm's source"),
215 "a seeded row does not say where it came from")
216 uassert.True(t, strings.Contains(seeded, "spendable now"))
217
218 query := Render(moul.String() + "?o=1000000&s=100&e=200")
219 uassert.True(t, strings.Contains(query, "supplied in the URL by you"),
220 "a query schedule does not say it was supplied")
221 uassert.True(t, strings.Contains(query, "Nothing was stored."))
222
223 unknown := Render(testutils.TestAddress("nobody").String())
224 uassert.True(t, strings.Contains(unknown, "No schedule on file"))
225 uassert.True(t, strings.Contains(unknown, "gnokey query auth/accounts/"),
226 "the unknown page does not say how to find the schedule")
227
228 bad := Render("not-an-address")
229 uassert.True(t, strings.Contains(bad, "is not a gno.land address"))
230}
231
232// TestRenderEscapesWhatTheCallerTyped: the path is attacker-controlled and a
233// live realm can never be patched, so the escaping is checked before deploy.
234func TestRenderEscapesWhatTheCallerTyped(t *testing.T) {
235 out := Render("[x](/r/evil)")
236 uassert.False(t, strings.Contains(out, "[x](/r/evil)"),
237 "a markdown link typed into the path reached the page unescaped")
238}
239
240// TestRenderQueryRejectsGarbage keeps a typo readable instead of rendering a
241// schedule of zeroes as though it were real.
242func TestRenderQueryRejectsGarbage(t *testing.T) {
243 out := Render(moul.String() + "?o=abc")
244 uassert.True(t, strings.Contains(out, "is not a number"))
245
246 out = Render(moul.String() + "?o=100&s=200&e=100")
247 uassert.True(t, strings.Contains(out, "not a valid schedule"))
248}
249
250// TestChainHeightIsReadable guards the one call that would make Declare
251// non-deterministic if it were wrong.
252func TestChainHeightIsReadable(t *testing.T) {
253 uassert.Equal(t, runtime.ChainHeight(), chainHeight())
254}
255
256// TestIndexIsBounded is the defect this realm shipped with: renderIndex
257// iterated EVERY registry row, and Declare is open to anyone, so the front
258// page grew without limit until it exceeded the query gas ceiling and stopped
259// rendering for everybody.
260//
261// vm/qrender runs on a gas meter. A realm that returns a large string fails
262// with OutOfGasError{Descriptor:"CPUCycles"} rather than answering slowly, and
263// the safe budget is tens of KB. A row is ~120 bytes, so a few hundred
264// declarations was enough. The realm is private = true, so the only repair
265// would have been a redeploy, which wipes every declaration and re-charges the
266// whole storage deposit.
267func TestIndexIsBounded(cur realm, t *testing.T) {
268 reset()
269 for i := 0; i < 400; i++ {
270 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress(ufmt.Sprintf("spam%d", i))))
271 Declare(cross(cur), 1000, 100, 200, false)
272 }
273 uassert.Equal(t, 400, Count(), "the registry should hold every declaration")
274
275 out := Render("")
276 uassert.True(t, len(out) < 12000,
277 ufmt.Sprintf("the index rendered %d bytes for 401 rows; it must page", len(out)))
278
279 // Paging still has to reach them, so page 2 differs from page 1.
280 uassert.True(t, Render("?page=2") != out, "?page=2 rendered the same page as page 1")
281 reset()
282}