Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}