draw.gno
3.51 Kb · 97 lines
1package planes
2
3import (
4 "chain/runtime"
5 "crypto/sha256"
6 "encoding/binary"
7 "time"
8)
9
10// Planes are minted in a random order: each mint draws one of the ids not
11// minted yet. The id drawn is the token id and the row of the table, so
12// whoever draws 777 gets the SuperGnome.
13//
14// The pool of ids left is a byte string of two bytes per id, big-endian,
15// 1554 bytes at deploy: a slice of 777 int64 would cost several times the
16// storage. A draw takes the id at a random index and moves the last id into
17// its place (swap-remove), so every id comes out exactly once and a draw
18// costs the same at any point of the sale.
19//
20// The randomness is pseudo-random, from what a realm can see: the sha256 of
21// the block height, the block time, the minter, the count minted so far and
22// the hash of the previous draw. A validator could nudge the block time, a
23// user could try to time their block; nobody can see a draw and undo it,
24// because Mint only accepts direct user calls (a contract cannot call Mint,
25// look at the id and panic to try again). That is enough for this
26// collection; it is not for anything with money riding on the outcome.
27
28// pool holds the ids not minted yet, two bytes each. Its length over two is
29// how many are left.
30var pool []byte
31
32// mintedBits has one bit per id, set once the id is minted (burned or not):
33// what tells a burned plane from one not minted yet, without scanning pool.
34var mintedBits []byte
35
36// lastDraw is the hash of the previous draw, chained into the next.
37var lastDraw [32]byte
38
39// testDraw, when set by a test, replaces the hash to pick an index in a pool
40// of n ids. It is never set outside the tests.
41var testDraw func(n int) int
42
43// resetPool fills the pool with every id, from 777 down to 1, and seeds the
44// chain of draws from the realm's path.
45func resetPool(pkgPath string) {
46 pool = make([]byte, 2*supply)
47 for i := int64(0); i < supply; i++ {
48 binary.BigEndian.PutUint16(pool[2*i:], uint16(supply-i))
49 }
50 mintedBits = make([]byte, (supply+7)/8)
51 lastDraw = sha256.Sum256([]byte(pkgPath))
52}
53
54// left returns how many ids are not minted yet.
55func left() int64 {
56 return int64(len(pool) / 2)
57}
58
59// drawHash is the hash a draw picks its index from. It is a pure function of
60// its inputs, so that the tests can check what moves it.
61func drawHash(height, nanos int64, minter address, minted int64, prev [32]byte) [32]byte {
62 buf := make([]byte, 0, 8+8+len(minter)+8+32)
63 buf = binary.BigEndian.AppendUint64(buf, uint64(height))
64 buf = binary.BigEndian.AppendUint64(buf, uint64(nanos))
65 buf = append(buf, minter...)
66 buf = binary.BigEndian.AppendUint64(buf, uint64(minted))
67 buf = append(buf, prev[:]...)
68 return sha256.Sum256(buf)
69}
70
71// drawID takes one id out of the pool, at random, for minter, and marks it
72// minted. The pool must not be empty.
73func drawID(minter address) int64 {
74 n := len(pool) / 2
75 var i int
76 if testDraw != nil {
77 i = testDraw(n)
78 } else {
79 lastDraw = drawHash(runtime.ChainHeight(), time.Now().UnixNano(), minter, Minted(), lastDraw)
80 // The first 8 bytes modulo the pool size: the bias of a modulo of
81 // 2^64 by at most 777 is under 777/2^64, nothing.
82 i = int(binary.BigEndian.Uint64(lastDraw[:8]) % uint64(n))
83 }
84 id := int64(binary.BigEndian.Uint16(pool[2*i:]))
85 copy(pool[2*i:2*i+2], pool[2*(n-1):2*n])
86 pool = pool[:2*(n-1)]
87 mintedBits[(id-1)/8] |= 1 << uint((id-1)%8)
88 return id
89}
90
91// wasMinted reports whether id was ever minted, burned since or not.
92func wasMinted(id int64) bool {
93 if id < 1 || id > supply {
94 return false
95 }
96 return mintedBits[(id-1)/8]&(1<<uint((id-1)%8)) != 0
97}