connect4_test.gno
23.86 Kb · 662 lines
1package connect4_v4
2
3import (
4 "chain"
5 "chain/banker"
6 "crypto/bech32"
7 "crypto/sha256"
8 "strconv"
9 "strings"
10 "testing"
11
12 "gno.land/p/nt/avl/v0"
13 "gno.land/p/nt/ownable/v0"
14 "gno.land/p/nt/testutils/v0"
15 "gno.land/p/nt/uassert/v0"
16)
17
18const stake = int64(10_000_000)
19
20var (
21 alice = testutils.TestAddress("alice")
22 bob = testutils.TestAddress("bob")
23 carol = testutils.TestAddress("carol")
24 admin = address("g1cvr48r7l7lkmvp77cr6zg2zhu26jgfwr0y8pew")
25 pkgAddr = chain.PackageAddress("gno.land/r/samcrew/connect4")
26)
27
28func reset() {
29 games, active, stats = avl.Tree{}, avl.Tree{}, avl.Tree{}
30 nextID, fee, feesCollected = 0, 100_000, 0
31 topWins, topGnot = nil, nil
32 used, pendingOwner = avl.Tree{}, ""
33 owner = ownable.NewWithAddress(admin)
34 testSession = false
35}
36
37// pay sets the ugnot sent with the next call and credits the realm with it,
38// mirroring what the chain does. testing.SetRealm must be called in the same
39// frame that makes the cross call, so tests call it inline rather than here.
40func pay(amount int64) {
41 if amount == 0 {
42 testing.SetOriginSend(nil)
43 return
44 }
45 coins := chain.Coins{{"ugnot", amount}}
46 testing.SetOriginSend(coins)
47 testing.IssueCoins(pkgAddr, coins)
48}
49
50func balance(addr address) int64 {
51 return banker.NewReadonlyBanker().GetCoin(addr, "ugnot")
52}
53
54// phrase returns a passphrase unique to the next game id.
55func phrase() string { return "four random words " + strconv.Itoa(nextID+1) }
56
57// commit returns the commitment for phrase().
58func commit() string { return sha256Hex(phrase()) }
59
60// seed returns the acceptor's seed for game id; seedCommit its commitment.
61func seed(id int) string { return "random seed " + strconv.Itoa(id) }
62func seedCommit(id int) string { return sha256Hex(seed(id)) }
63
64// accepted returns a game between alice (creator) and bob awaiting Reveal,
65// and its passphrase.
66func accepted(cur realm) (*Game, string) {
67 p := phrase()
68 testing.SetRealm(testing.NewUserRealm(alice))
69 pay(stake)
70 id := Offer(cross(cur), "", 10, sha256Hex(p), fee)
71 testing.SetRealm(testing.NewUserRealm(bob))
72 pay(stake)
73 Accept(cross(cur), id, seedCommit(id))
74 return mustGet(id), p
75}
76
77// started returns a revealed Playing game between alice (creator) and bob.
78func started(cur realm) *Game {
79 g, p := accepted(cur)
80 revealBoth(cur, g, p)
81 return g
82}
83
84// revealBoth has the creator reveal p and the acceptor its seed.
85func revealBoth(cur realm, g *Game, p string) {
86 testing.SetRealm(testing.NewUserRealm(g.Creator))
87 pay(0)
88 Reveal(cross(cur), g.ID, p)
89 testing.SetRealm(testing.NewUserRealm(g.Acceptor))
90 pay(0)
91 RevealSeed(cross(cur), g.ID, seed(g.ID))
92}
93
94// move plays column for whoever is on turn.
95func move(cur realm, g *Game, column int) {
96 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
97 pay(0)
98 Play(cross(cur), g.ID, column, g.Moves)
99}
100
101// expireClock advances block time past the 90s move clock (SkipHeights is 5s/block).
102func expireClock() { testing.SkipHeights(19) }
103
104func TestWinPaysPotMinusFee(cur realm, t *testing.T) {
105 reset()
106 g := started(cur)
107 first := g.player(g.Turn)
108 before := balance(first)
109 for _, c := range []int{1, 2, 1, 2, 1, 2, 1} { // first mover stacks column 1
110 move(cur, g, c)
111 }
112 uassert.Equal(t, "won", g.Status.String())
113 uassert.Equal(t, first, g.Winner)
114 uassert.Equal(t, 2*stake-100_000, balance(first)-before)
115 uassert.Equal(t, int64(100_000), feesCollected)
116 uassert.Equal(t, 1, statsOf(first).Wins)
117 uassert.Equal(t, stake-100_000, statsOf(first).GnotWon)
118 uassert.Equal(t, 1, statsOf(g.player(3-g.Turn)).Games)
119 uassert.Equal(t, 0, active.Size())
120}
121
122func TestOfferRejects(cur realm, t *testing.T) {
123 reset()
124 testing.SetRealm(testing.NewUserRealm(alice))
125 pay(minStake - 1)
126 uassert.AbortsContains(t, cur, "stake below minimum", func() { Offer(cross(cur), "", 10, commit(), fee) })
127 testing.SetRealm(testing.NewUserRealm(alice))
128 pay(stake)
129 uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 0, commit(), fee) })
130 testing.SetRealm(testing.NewUserRealm(alice))
131 pay(stake)
132 uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 61, commit(), fee) })
133 testing.SetRealm(testing.NewUserRealm(alice))
134 pay(stake)
135 uassert.AbortsContains(t, cur, "yourself", func() { Offer(cross(cur), alice, 10, commit(), fee) })
136 testing.SetRealm(testing.NewUserRealm(alice))
137 pay(0)
138 uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit(), fee) })
139 testing.SetRealm(testing.NewUserRealm(alice))
140 testing.SetOriginSend(chain.Coins{{"ugnot", stake}, {"foo", 1}})
141 uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit(), fee) })
142}
143
144func TestAcceptRejects(cur realm, t *testing.T) {
145 reset()
146 testing.SetRealm(testing.NewUserRealm(alice))
147 pay(stake)
148 id := Offer(cross(cur), bob, 1, commit(), fee)
149 testing.SetRealm(testing.NewUserRealm(alice))
150 pay(stake)
151 uassert.AbortsContains(t, cur, "own offer", func() { Accept(cross(cur), id, seedCommit(id)) })
152 testing.SetRealm(testing.NewUserRealm(carol))
153 pay(stake)
154 uassert.AbortsContains(t, cur, "reserved", func() { Accept(cross(cur), id, seedCommit(id)) })
155 testing.SetRealm(testing.NewUserRealm(bob))
156 pay(stake - 1)
157 uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id, seedCommit(id)) })
158 testing.SetRealm(testing.NewUserRealm(bob))
159 pay(stake + 1)
160 uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id, seedCommit(id)) })
161 testing.SkipHeights(13) // 65s > 1 minute
162 testing.SetRealm(testing.NewUserRealm(bob))
163 pay(stake)
164 uassert.AbortsContains(t, cur, "expired", func() { Accept(cross(cur), id, seedCommit(id)) })
165 testing.SetRealm(testing.NewUserRealm(bob))
166 pay(stake)
167 uassert.AbortsContains(t, cur, "not found", func() { Accept(cross(cur), 99, seedCommit(99)) })
168}
169
170func TestPlayRejects(cur realm, t *testing.T) {
171 reset()
172 g := started(cur)
173 testing.SetRealm(testing.NewUserRealm(g.player(3 - g.Turn)))
174 pay(0)
175 uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1, g.Moves) })
176 testing.SetRealm(testing.NewUserRealm(carol))
177 pay(0)
178 uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1, g.Moves) })
179 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
180 pay(0)
181 uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 0, g.Moves) })
182 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
183 pay(0)
184 uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 8, g.Moves) })
185 expireClock()
186 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
187 pay(0)
188 uassert.AbortsContains(t, cur, "clock ran out", func() { Play(cross(cur), g.ID, 1, g.Moves) })
189}
190
191func TestFullColumnRejected(cur realm, t *testing.T) {
192 reset()
193 g := started(cur)
194 for i := 0; i < rows; i++ {
195 move(cur, g, 4) // alternating players never make 4 in a column
196 }
197 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
198 pay(0)
199 uassert.AbortsContains(t, cur, "column is full", func() { Play(cross(cur), g.ID, 4, g.Moves) })
200}
201
202// The creator revealed, then the first mover never moved.
203func TestTimeoutBeforeFirstMoveIsVoid(cur realm, t *testing.T) {
204 reset()
205 g := started(cur)
206 testing.SetRealm(testing.NewUserRealm(carol))
207 pay(0)
208 uassert.AbortsContains(t, cur, "has not run out", func() { ClaimTimeout(cross(cur), g.ID) })
209 a, b := balance(alice), balance(bob)
210 expireClock()
211 testing.SetRealm(testing.NewUserRealm(carol))
212 pay(0) // anyone may settle
213 ClaimTimeout(cross(cur), g.ID)
214 uassert.Equal(t, "void", g.Status.String())
215 uassert.Equal(t, stake, balance(alice)-a)
216 uassert.Equal(t, stake, balance(bob)-b)
217 uassert.Equal(t, int64(0), feesCollected)
218 uassert.Equal(t, 0, stats.Size())
219}
220
221func TestTimeoutAfterMoveForfeits(cur realm, t *testing.T) {
222 reset()
223 g := started(cur)
224 mover := g.player(g.Turn)
225 loser := g.player(3 - g.Turn)
226 move(cur, g, 1)
227 expireClock()
228 before := balance(mover)
229 testing.SetRealm(testing.NewUserRealm(mover))
230 pay(0)
231 ClaimTimeout(cross(cur), g.ID)
232 uassert.Equal(t, "won", g.Status.String())
233 uassert.Equal(t, mover, g.Winner)
234 uassert.Equal(t, 2*stake-100_000, balance(mover)-before)
235 uassert.Equal(t, int64(100_000), feesCollected)
236 uassert.Equal(t, 1, statsOf(loser).Games)
237}
238
239func TestResign(cur realm, t *testing.T) {
240 reset()
241 g := started(cur)
242 testing.SetRealm(testing.NewUserRealm(carol))
243 pay(0)
244 uassert.AbortsContains(t, cur, "not a player", func() { Resign(cross(cur), g.ID) })
245 before := balance(bob)
246 testing.SetRealm(testing.NewUserRealm(alice))
247 pay(0)
248 Resign(cross(cur), g.ID)
249 uassert.Equal(t, "won", g.Status.String())
250 uassert.Equal(t, bob, g.Winner)
251 uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
252 uassert.Equal(t, int64(100_000), feesCollected)
253 uassert.Equal(t, 1, statsOf(alice).Games)
254}
255
256func TestCancel(cur realm, t *testing.T) {
257 reset()
258 testing.SetRealm(testing.NewUserRealm(alice))
259 pay(stake)
260 id := Offer(cross(cur), "", 1, commit(), fee)
261 testing.SetRealm(testing.NewUserRealm(bob))
262 pay(0)
263 uassert.AbortsContains(t, cur, "only the creator", func() { Cancel(cross(cur), id) })
264 before := balance(alice)
265 testing.SetRealm(testing.NewUserRealm(alice))
266 pay(0)
267 Cancel(cross(cur), id) // creator, before expiry
268 uassert.Equal(t, "cancelled", mustGet(id).Status.String())
269 uassert.Equal(t, stake, balance(alice)-before)
270
271 // Once expired, anyone may cancel; the refund still goes to the creator.
272 testing.SetRealm(testing.NewUserRealm(alice))
273 pay(stake)
274 id = Offer(cross(cur), "", 1, commit(), fee)
275 testing.SkipHeights(13) // 65s > 1 minute
276 before = balance(alice)
277 testing.SetRealm(testing.NewUserRealm(carol))
278 pay(0)
279 Cancel(cross(cur), id)
280 uassert.Equal(t, "cancelled", mustGet(id).Status.String())
281 uassert.Equal(t, stake, balance(alice)-before)
282
283 g := started(cur)
284 testing.SetRealm(testing.NewUserRealm(alice))
285 pay(0)
286 uassert.AbortsContains(t, cur, "not open", func() { Cancel(cross(cur), g.ID) })
287}
288
289func TestDrawRefundsBoth(cur realm, t *testing.T) {
290 reset()
291 g := started(cur)
292 // Fill every cell but the top of column 7 with a pattern that has no
293 // four in a row; player 1 owns the last cell.
294 for c := 0; c < cols; c++ {
295 for r := 0; r < rows; r++ {
296 g.Board[c][r] = byte(1 + (c/2+r)%2)
297 }
298 }
299 g.Board[6][5] = 0
300 g.Moves, g.Turn = cols*rows-1, 1
301 a, b := balance(alice), balance(bob)
302 move(cur, g, 7)
303 uassert.Equal(t, "draw", g.Status.String())
304 uassert.Equal(t, stake, balance(alice)-a)
305 uassert.Equal(t, stake, balance(bob)-b)
306 uassert.Equal(t, 1, statsOf(alice).Draws)
307}
308
309func TestFeeSnapshotAndWithdraw(cur realm, t *testing.T) {
310 reset()
311 testing.SetRealm(testing.NewUserRealm(carol))
312 pay(0)
313 uassert.AbortsContains(t, cur, "not owner", func() { SetFee(cross(cur), 0) })
314 testing.SetRealm(testing.NewUserRealm(carol))
315 pay(0)
316 uassert.AbortsContains(t, cur, "not owner", func() { WithdrawFees(cross(cur), carol) })
317 g := started(cur) // offered at the default 0.1 GNOT fee
318 testing.SetRealm(testing.NewUserRealm(admin))
319 pay(0)
320 uassert.AbortsContains(t, cur, "fee must be between", func() { SetFee(cross(cur), -1) })
321 uassert.AbortsContains(t, cur, "fee must be between 0 and 0.5 GNOT", func() { SetFee(cross(cur), feeCap+1) })
322 SetFee(cross(cur), feeCap)
323 testing.SetRealm(testing.NewUserRealm(alice))
324 pay(0)
325 Resign(cross(cur), g.ID)
326 uassert.Equal(t, int64(100_000), feesCollected)
327
328 before := balance(carol)
329 testing.SetRealm(testing.NewUserRealm(admin))
330 pay(0)
331 WithdrawFees(cross(cur), carol)
332 uassert.Equal(t, int64(100_000), balance(carol)-before)
333 uassert.Equal(t, int64(0), feesCollected)
334
335 // A raised fee applies to new offers only, and stays below the minimum stake.
336 testing.SetRealm(testing.NewUserRealm(alice))
337 pay(minStake)
338 id := Offer(cross(cur), "", 10, commit(), fee)
339 uassert.Equal(t, int64(feeCap), mustGet(id).Fee)
340}
341
342func TestFinishedGameRejects(cur realm, t *testing.T) {
343 reset()
344 g := started(cur)
345 testing.SetRealm(testing.NewUserRealm(alice))
346 pay(0)
347 Resign(cross(cur), g.ID)
348 testing.SetRealm(testing.NewUserRealm(bob))
349 pay(0)
350 uassert.AbortsContains(t, cur, "not in progress", func() { Play(cross(cur), g.ID, 1, g.Moves) })
351 uassert.AbortsContains(t, cur, "not in progress", func() { Resign(cross(cur), g.ID) })
352 expireClock()
353 testing.SetRealm(testing.NewUserRealm(bob))
354 pay(0)
355 uassert.AbortsContains(t, cur, "not in progress", func() { ClaimTimeout(cross(cur), g.ID) })
356 testing.SetRealm(testing.NewUserRealm(carol))
357 pay(stake)
358 uassert.AbortsContains(t, cur, "not open", func() { Accept(cross(cur), g.ID, seedCommit(g.ID)) })
359 testing.SetRealm(testing.NewUserRealm(carol))
360 pay(stake)
361 uassert.AbortsContains(t, cur, "invalid opponent", func() { Offer(cross(cur), "notanaddress", 10, commit(), fee) })
362}
363
364func TestRevealPhase(cur realm, t *testing.T) {
365 reset()
366 g, p := accepted(cur)
367 uassert.Equal(t, byte(0), g.Turn)
368 for _, who := range []address{alice, bob} {
369 testing.SetRealm(testing.NewUserRealm(who))
370 pay(0)
371 uassert.AbortsContains(t, cur, "waiting for the creator", func() { Play(cross(cur), g.ID, 1, g.Moves) })
372 }
373 testing.SetRealm(testing.NewUserRealm(bob))
374 pay(0)
375 uassert.AbortsContains(t, cur, "only the creator", func() { Reveal(cross(cur), g.ID, p) })
376 testing.SetRealm(testing.NewUserRealm(alice))
377 pay(0)
378 uassert.AbortsContains(t, cur, "does not match", func() { Reveal(cross(cur), g.ID, p+"x") })
379 testing.SetRealm(testing.NewUserRealm(bob))
380 pay(0)
381 uassert.AbortsContains(t, cur, "not awaiting the acceptor's seed", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
382 testing.SetRealm(testing.NewUserRealm(alice))
383 pay(0)
384 Reveal(cross(cur), g.ID, p)
385 uassert.Equal(t, byte(0), g.Turn) // the draw waits for the seed
386 uassert.True(t, g.Revealed)
387 testing.SetRealm(testing.NewUserRealm(alice))
388 pay(0)
389 uassert.AbortsContains(t, cur, "not awaiting the creator's reveal", func() { Reveal(cross(cur), g.ID, p) })
390 uassert.AbortsContains(t, cur, "only the acceptor", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
391 testing.SetRealm(testing.NewUserRealm(bob))
392 pay(0)
393 uassert.AbortsContains(t, cur, "seed does not match", func() { RevealSeed(cross(cur), g.ID, "other") })
394 testing.SetRealm(testing.NewUserRealm(bob))
395 pay(0)
396 RevealSeed(cross(cur), g.ID, seed(g.ID))
397 uassert.True(t, g.Turn == 1 || g.Turn == 2)
398 testing.SetRealm(testing.NewUserRealm(bob))
399 pay(0)
400 uassert.AbortsContains(t, cur, "not awaiting the acceptor's seed", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
401 move(cur, g, 1) // the picked first mover can play
402 uassert.Equal(t, 1, g.Moves)
403}
404
405func TestLateRevealForfeits(cur realm, t *testing.T) {
406 reset()
407 g, p := accepted(cur)
408 expireClock()
409 testing.SetRealm(testing.NewUserRealm(alice))
410 pay(0)
411 uassert.AbortsContains(t, cur, "reveal clock ran out", func() { Reveal(cross(cur), g.ID, p) })
412 before := balance(bob)
413 testing.SetRealm(testing.NewUserRealm(carol))
414 pay(0)
415 ClaimTimeout(cross(cur), g.ID)
416 uassert.Equal(t, "won", g.Status.String())
417 uassert.Equal(t, bob, g.Winner)
418 uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
419 uassert.Equal(t, int64(100_000), feesCollected)
420 uassert.Equal(t, 1, statsOf(bob).Wins)
421 uassert.Equal(t, 1, statsOf(alice).Games)
422}
423
424func TestLateSeedForfeits(cur realm, t *testing.T) {
425 reset()
426 g, p := accepted(cur)
427 testing.SetRealm(testing.NewUserRealm(alice))
428 pay(0)
429 Reveal(cross(cur), g.ID, p)
430 expireClock()
431 testing.SetRealm(testing.NewUserRealm(bob))
432 pay(0)
433 uassert.AbortsContains(t, cur, "reveal clock ran out", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
434 before := balance(alice)
435 testing.SetRealm(testing.NewUserRealm(carol))
436 pay(0)
437 ClaimTimeout(cross(cur), g.ID)
438 uassert.Equal(t, alice, g.Winner)
439 uassert.Equal(t, 2*stake-100_000, balance(alice)-before)
440}
441
442func TestCommitmentRejects(cur realm, t *testing.T) {
443 reset()
444 good := commit()
445 for _, bad := range []string{"", good[:63], good + "0", "G" + good[1:], "A" + good[1:]} {
446 testing.SetRealm(testing.NewUserRealm(alice))
447 pay(stake)
448 uassert.AbortsContains(t, cur, "64 lowercase hex", func() { Offer(cross(cur), "", 10, bad, fee) })
449 }
450 testing.SetRealm(testing.NewUserRealm(alice))
451 pay(stake)
452 Offer(cross(cur), "", 10, good, fee)
453 testing.SetRealm(testing.NewUserRealm(carol))
454 pay(stake)
455 Offer(cross(cur), "", 10, good, fee) // another creator may use it: nobody can block anyone's offer
456 testing.SetRealm(testing.NewUserRealm(alice))
457 pay(stake)
458 uassert.AbortsContains(t, cur, "commitment already used", func() { Offer(cross(cur), "", 10, good, fee) })
459
460 // An acceptor can't reuse a seed commitment either, even one used as a passphrase commitment.
461 testing.SetRealm(testing.NewUserRealm(bob))
462 pay(stake)
463 Accept(cross(cur), 1, seedCommit(1))
464 testing.SetRealm(testing.NewUserRealm(bob))
465 pay(stake)
466 uassert.AbortsContains(t, cur, "commitment already used", func() { Accept(cross(cur), 2, seedCommit(1)) })
467 testing.SetRealm(testing.NewUserRealm(bob))
468 pay(stake)
469 uassert.AbortsContains(t, cur, "seedCommitment must be 64 lowercase hex", func() { Accept(cross(cur), 2, "x") })
470}
471
472// bech32m gives the other checksum of the same address, which IsValid accepts.
473func bech32m(a address) address {
474 _, data, err := bech32.DecodeNoLimit(a.String())
475 if err != nil {
476 panic(err)
477 }
478 m, err := bech32.EncodeM("g", data)
479 if err != nil {
480 panic(err)
481 }
482 return address(m)
483}
484
485func TestOpponentMustBeCanonical(cur realm, t *testing.T) {
486 reset()
487 for _, other := range []address{
488 address(strings.ToUpper(bob.String())),
489 bech32m(bob),
490 address(strings.ToUpper(bech32m(bob).String())),
491 } {
492 uassert.True(t, other.IsValid())
493 testing.SetRealm(testing.NewUserRealm(alice))
494 pay(stake)
495 uassert.AbortsContains(t, cur, "lower-case", func() { Offer(cross(cur), other, 10, commit(), fee) })
496 }
497 testing.SetRealm(testing.NewUserRealm(alice))
498 pay(stake)
499 Offer(cross(cur), bob, 10, commit(), fee)
500}
501
502func TestOfferRefusesAFeeAboveItsMaximum(cur realm, t *testing.T) {
503 reset()
504 testing.SetRealm(testing.NewUserRealm(admin))
505 pay(0)
506 SetFee(cross(cur), 300_000) // raised while the creator's offer is in flight
507 testing.SetRealm(testing.NewUserRealm(alice))
508 pay(stake)
509 uassert.AbortsContains(t, cur, "the house fee is 0.3 GNOT, above your maximum of 0.1 GNOT", func() {
510 Offer(cross(cur), "", 10, commit(), 100_000)
511 })
512 testing.SetRealm(testing.NewUserRealm(alice))
513 pay(stake)
514 id := Offer(cross(cur), "", 10, commit(), 300_000)
515 uassert.Equal(t, int64(300_000), mustGet(id).Fee)
516 testing.SetRealm(testing.NewUserRealm(alice))
517 pay(stake)
518 id = Offer(cross(cur), "", 10, commit(), 1_000_000) // a higher maximum still pays only the current fee
519 uassert.Equal(t, int64(300_000), mustGet(id).Fee)
520}
521
522func TestOwnershipTransferTakesTwoSteps(cur realm, t *testing.T) {
523 reset()
524 testing.SetRealm(testing.NewUserRealm(carol))
525 pay(0)
526 uassert.AbortsContains(t, cur, "not owner", func() { TransferOwnership(cross(cur), carol) })
527 // A non-canonical spelling, which could never accept, is refused up front.
528 for _, other := range []address{address(strings.ToUpper(carol.String())), bech32m(carol)} {
529 testing.SetRealm(testing.NewUserRealm(admin))
530 pay(0)
531 uassert.AbortsContains(t, cur, "invalid address", func() { TransferOwnership(cross(cur), other) })
532 }
533 testing.SetRealm(testing.NewUserRealm(carol))
534 pay(0)
535 uassert.AbortsContains(t, cur, "no ownership transfer pending", func() { AcceptOwnership(cross(cur)) })
536 testing.SetRealm(testing.NewUserRealm(admin))
537 pay(0)
538 SetFee(cross(cur), 0) // still the owner
539 TransferOwnership(cross(cur), carol)
540 testing.SetRealm(testing.NewUserRealm(bob))
541 pay(0)
542 uassert.AbortsContains(t, cur, "no ownership transfer pending", func() { AcceptOwnership(cross(cur)) })
543 testing.SetRealm(testing.NewUserRealm(carol))
544 pay(0)
545 AcceptOwnership(cross(cur))
546 SetFee(cross(cur), 1)
547 testing.SetRealm(testing.NewUserRealm(admin))
548 pay(0)
549 uassert.AbortsContains(t, cur, "not owner", func() { SetFee(cross(cur), 0) })
550}
551
552func TestPaymentsRequireUserCall(cur realm, t *testing.T) {
553 reset()
554 testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
555 pay(stake)
556 uassert.AbortsContains(t, cur, "only user-call", func() { Offer(cross(cur), "", 10, commit(), fee) })
557 testing.SetRealm(testing.NewUserRealm(alice))
558 pay(stake)
559 id := Offer(cross(cur), "", 10, commit(), fee)
560 testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
561 pay(stake)
562 uassert.AbortsContains(t, cur, "only user-call", func() { Accept(cross(cur), id, seedCommit(id)) })
563}
564
565func TestNonPayableRejectsCoins(cur realm, t *testing.T) {
566 reset()
567 g, p := accepted(cur)
568 calls := map[string]func(){
569 "Reveal": func() { Reveal(cross(cur), g.ID, p) },
570 "RevealSeed": func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) },
571 "AcceptOwnership": func() { AcceptOwnership(cross(cur)) },
572 "Play": func() { Play(cross(cur), g.ID, 1, g.Moves) },
573 "ClaimTimeout": func() { ClaimTimeout(cross(cur), g.ID) },
574 "Resign": func() { Resign(cross(cur), g.ID) },
575 "Cancel": func() { Cancel(cross(cur), g.ID) },
576 "SetFee": func() { SetFee(cross(cur), 0) },
577 "WithdrawFees": func() { WithdrawFees(cross(cur), admin) },
578 "TransferOwnership": func() { TransferOwnership(cross(cur), carol) },
579 }
580 for name, call := range calls {
581 testing.SetRealm(testing.NewUserRealm(admin))
582 pay(1)
583 uassert.AbortsContains(t, cur, "does not accept coins", call, name)
584 }
585}
586
587func TestDrawUsesBothSecrets(cur realm, t *testing.T) {
588 // The draw is fixed by the passphrase and the seed, whenever the reveals land.
589 reset()
590 g, p := accepted(cur)
591 testing.SkipHeights(5) // 25s, inside the reveal clocks
592 revealBoth(cur, g, p)
593 d := sha256.Sum256([]byte(strings.Join([]string{p, seed(g.ID), strconv.Itoa(g.ID), alice.String(), bob.String()}, "|")))
594 uassert.Equal(t, 1+d[0]&1, g.Turn)
595 // The creator can't fix the outcome alone: the same passphrase gives both
596 // first movers depending on the acceptor's seed.
597 firsts := map[byte]bool{}
598 for i := 0; i < 32 && len(firsts) < 2; i++ {
599 firsts[firstMover(p, "seed "+strconv.Itoa(i), g)] = true
600 }
601 uassert.Equal(t, 2, len(firsts))
602}
603
604func TestPlayRefusesStaleMove(cur realm, t *testing.T) {
605 reset()
606 g := started(cur)
607 stale := g.Moves
608 move(cur, g, 1)
609 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
610 pay(0)
611 uassert.AbortsContains(t, cur, "stale move: the game is at move 1", func() { Play(cross(cur), g.ID, 2, stale) })
612 uassert.AbortsContains(t, cur, "stale move", func() { Play(cross(cur), g.ID, 2, g.Moves+1) })
613 Play(cross(cur), g.ID, 2, g.Moves)
614 uassert.Equal(t, 2, g.Moves)
615}
616
617func TestSessionKeysCannotResignStakeOrOwn(cur realm, t *testing.T) {
618 reset()
619 g := started(cur)
620 testSession = true
621 testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
622 pay(0)
623 uassert.AbortsContains(t, cur, "Resign needs the account's own key", func() { Resign(cross(cur), g.ID) })
624 testing.SetRealm(testing.NewUserRealm(alice))
625 pay(stake)
626 uassert.AbortsContains(t, cur, "Offer needs the account's own key", func() { Offer(cross(cur), "", 10, commit(), fee) })
627 testing.SetRealm(testing.NewUserRealm(admin))
628 pay(0)
629 uassert.AbortsContains(t, cur, "SetFee needs", func() { SetFee(cross(cur), 0) })
630 uassert.AbortsContains(t, cur, "WithdrawFees needs", func() { WithdrawFees(cross(cur), admin) })
631 uassert.AbortsContains(t, cur, "TransferOwnership needs", func() { TransferOwnership(cross(cur), carol) })
632 // Moves stay open to sessions: that is what Quick play is for.
633 move(cur, g, 1)
634 uassert.Equal(t, 1, g.Moves)
635}
636
637func TestSessionKeysCannotAccept(cur realm, t *testing.T) {
638 reset()
639 testing.SetRealm(testing.NewUserRealm(alice))
640 pay(stake)
641 id := Offer(cross(cur), "", 10, commit(), fee)
642 testSession = true
643 testing.SetRealm(testing.NewUserRealm(bob))
644 pay(stake)
645 uassert.AbortsContains(t, cur, "Accept needs the account's own key", func() { Accept(cross(cur), id, seedCommit(id)) })
646}
647
648func TestBump(t *testing.T) {
649 var b []leader
650 for i := 0; i < 12; i++ {
651 b = bump(b, testutils.TestAddress(strconv.Itoa(i)), int64(i))
652 }
653 uassert.Equal(t, topN, len(b))
654 uassert.Equal(t, int64(11), b[0].Score)
655 uassert.Equal(t, int64(2), b[9].Score)
656 b = bump(b, testutils.TestAddress("5"), 20) // already listed: moves up, not duplicated
657 uassert.Equal(t, topN, len(b))
658 uassert.Equal(t, testutils.TestAddress("5"), b[0].Addr)
659 b = bump(b, testutils.TestAddress("late"), 11) // ties keep the earlier holder first
660 uassert.Equal(t, testutils.TestAddress("11"), b[1].Addr)
661 uassert.Equal(t, testutils.TestAddress("late"), b[2].Addr)
662}