File: miner_test.go

package info (click to toggle)
sia 1.3.0-4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 6,340 kB
  • sloc: makefile: 80; sh: 52
file content (347 lines) | stat: -rw-r--r-- 9,134 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package miner

import (
	"bytes"
	"path/filepath"
	"testing"
	"time"

	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/crypto"
	"github.com/NebulousLabs/Sia/modules"
	"github.com/NebulousLabs/Sia/modules/consensus"
	"github.com/NebulousLabs/Sia/modules/gateway"
	"github.com/NebulousLabs/Sia/modules/transactionpool"
	"github.com/NebulousLabs/Sia/modules/wallet"
	"github.com/NebulousLabs/Sia/types"
	"github.com/NebulousLabs/fastrand"
)

// A minerTester is the helper object for miner testing.
type minerTester struct {
	gateway   modules.Gateway
	cs        modules.ConsensusSet
	tpool     modules.TransactionPool
	wallet    modules.Wallet
	walletKey crypto.TwofishKey

	miner *Miner

	minedBlocks []types.Block
	persistDir  string
}

// createMinerTester creates a minerTester that's ready for use.
func createMinerTester(name string) (*minerTester, error) {
	testdir := build.TempDir(modules.MinerDir, name)

	// Create the modules.
	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
	if err != nil {
		return nil, err
	}
	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
	if err != nil {
		return nil, err
	}
	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
	if err != nil {
		return nil, err
	}
	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
	if err != nil {
		return nil, err
	}
	var key crypto.TwofishKey
	fastrand.Read(key[:])
	_, err = w.Encrypt(key)
	if err != nil {
		return nil, err
	}
	err = w.Unlock(key)
	if err != nil {
		return nil, err
	}
	m, err := New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
	if err != nil {
		return nil, err
	}

	// Assemble the minerTester.
	mt := &minerTester{
		gateway:   g,
		cs:        cs,
		tpool:     tp,
		wallet:    w,
		walletKey: key,

		miner: m,

		persistDir: testdir,
	}

	// Mine until the wallet has money.
	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
		b, err := m.AddBlock()
		if err != nil {
			return nil, err
		}
		mt.minedBlocks = append(mt.minedBlocks, b)
	}

	return mt, nil
}

// TestIntegrationMiner creates a miner, mines a few blocks, and checks that
// the wallet balance is updating as the blocks get mined.
func TestIntegrationMiner(t *testing.T) {
	if testing.Short() {
		t.SkipNow()
	}
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}

	// Check that the wallet has money.
	siacoins, _, _ := mt.wallet.ConfirmedBalance()
	if siacoins.IsZero() {
		t.Error("expecting mining full balance to not be zero")
	}

	// Mine a bunch of blocks.
	for i := 0; i < 50; i++ {
		b, _ := mt.miner.FindBlock()
		err = mt.cs.AcceptBlock(b)
		if err != nil {
			t.Fatal(err)
		}
	}
	morecoins, _, _ := mt.wallet.ConfirmedBalance()
	if siacoins.Cmp(morecoins) >= 0 {
		t.Error("wallet is not gaining balance while mining")
	}
}

// TestIntegrationNilMinerDependencies tests that the miner properly handles
// nil inputs for its dependencies.
func TestIntegrationNilMinerDependencies(t *testing.T) {
	if testing.Short() {
		t.SkipNow()
	}
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}
	_, err = New(mt.cs, mt.tpool, nil, "")
	if err != errNilWallet {
		t.Fatal(err)
	}
	_, err = New(mt.cs, nil, mt.wallet, "")
	if err != errNilTpool {
		t.Fatal(err)
	}
	_, err = New(nil, mt.tpool, mt.wallet, "")
	if err != errNilCS {
		t.Fatal(err)
	}
	_, err = New(nil, nil, nil, "")
	if err == nil {
		t.Fatal(err)
	}
}

// TestIntegrationBlocksMined checks that the BlocksMined function correctly
// indicates the number of real blocks and stale blocks that have been mined.
func TestIntegrationBlocksMined(t *testing.T) {
	if testing.Short() {
		t.SkipNow()
	}
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}

	// Get an unsolved header.
	unsolvedHeader, target, err := mt.miner.HeaderForWork()
	if err != nil {
		t.Fatal(err)
	}
	// Unsolve the header - necessary because the target is very low when
	// mining.
	for {
		unsolvedHeader.Nonce[0]++
		id := crypto.HashObject(unsolvedHeader)
		if bytes.Compare(target[:], id[:]) < 0 {
			break
		}
	}

	// Get two solved headers.
	header1, target, err := mt.miner.HeaderForWork()
	if err != nil {
		t.Fatal(err)
	}
	header1 = solveHeader(header1, target)
	header2, target, err := mt.miner.HeaderForWork()
	if err != nil {
		t.Fatal(err)
	}
	header2 = solveHeader(header2, target)

	// Submit the unsolved header followed by the two solved headers, this
	// should result in 1 real block mined and 1 stale block mined.
	err = mt.miner.SubmitHeader(unsolvedHeader)
	if err != modules.ErrBlockUnsolved {
		t.Fatal(err)
	}
	err = mt.miner.SubmitHeader(header1)
	if err != nil {
		t.Fatal(err)
	}
	err = mt.miner.SubmitHeader(header2)
	if err != modules.ErrNonExtendingBlock {
		t.Fatal(err)
	}
	goodBlocks, staleBlocks := mt.miner.BlocksMined()
	if goodBlocks != 1 {
		t.Error("expecting 1 good block")
	}
	if staleBlocks != 1 {
		t.Error("expecting 1 stale block, got", staleBlocks)
	}

	// Reboot the miner and verify that the block record has persisted.
	err = mt.miner.Close()
	if err != nil {
		t.Fatal(err)
	}
	rebootMiner, err := New(mt.cs, mt.tpool, mt.wallet, filepath.Join(mt.persistDir, modules.MinerDir))
	if err != nil {
		t.Fatal(err)
	}
	goodBlocks, staleBlocks = rebootMiner.BlocksMined()
	if goodBlocks != 1 {
		t.Error("expecting 1 good block")
	}
	if staleBlocks != 1 {
		t.Error("expecting 1 stale block, got", staleBlocks)
	}
}

// TestIntegrationAutoRescan triggers a rescan during a call to New and
// verifies that the rescanning happens correctly. The rescan is triggered by
// a call to New, instead of getting called directly.
func TestIntegrationAutoRescan(t *testing.T) {
	if testing.Short() {
		t.SkipNow()
	}
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}
	_, err = mt.miner.AddBlock()
	if err != nil {
		t.Fatal(err)
	}

	// Get the persist data of the current miner.
	oldChange := mt.miner.persist.RecentChange
	oldHeight := mt.miner.persist.Height
	oldTarget := mt.miner.persist.Target

	// Corrupt the miner, close the miner, and make a new one from the same
	// directory.
	mt.miner.persist.RecentChange[0]++
	mt.miner.persist.Height += 1e5
	mt.miner.persist.Target[0]++
	err = mt.miner.Close() // miner saves when it closes.
	if err != nil {
		t.Fatal(err)
	}

	// Verify that rescanning resolved the corruption in the miner.
	m, err := New(mt.cs, mt.tpool, mt.wallet, filepath.Join(mt.persistDir, modules.MinerDir))
	if err != nil {
		t.Fatal(err)
	}
	// Check that after rescanning, the values have returned to the usual values.
	if m.persist.RecentChange != oldChange {
		t.Error("rescan failed, ended up on the wrong change")
	}
	if m.persist.Height != oldHeight {
		t.Error("rescan failed, ended up at the wrong height")
	}
	if m.persist.Target != oldTarget {
		t.Error("rescan failed, ended up at the wrong target")
	}
}

// TestIntegrationStartupRescan probes the startupRescan function, checking
// that it works in the naive case. Rescan is called directly.
func TestIntegrationStartupRescan(t *testing.T) {
	if testing.Short() {
		t.SkipNow()
	}
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}

	// Check that the miner's persist variables have been initialized to the
	// first few blocks.
	if mt.miner.persist.RecentChange == (modules.ConsensusChangeID{}) || mt.miner.persist.Height == 0 || mt.miner.persist.Target == (types.Target{}) {
		t.Fatal("miner persist variables not initialized")
	}
	oldChange := mt.miner.persist.RecentChange
	oldHeight := mt.miner.persist.Height
	oldTarget := mt.miner.persist.Target

	// Corrupt the miner and verify that a rescan repairs the corruption.
	mt.miner.persist.RecentChange[0]++
	mt.miner.persist.Height += 500
	mt.miner.persist.Target[0]++
	mt.cs.Unsubscribe(mt.miner)
	err = mt.miner.startupRescan()
	if err != nil {
		t.Fatal(err)
	}
	if mt.miner.persist.RecentChange != oldChange {
		t.Error("rescan failed, ended up on the wrong change")
	}
	if mt.miner.persist.Height != oldHeight {
		t.Error("rescan failed, ended up at the wrong height")
	}
	if mt.miner.persist.Target != oldTarget {
		t.Error("rescan failed, ended up at the wrong target")
	}
}

// TestMinerCloseDeadlock checks that the miner can cleanly close even if the
// CPU miner is running.
func TestMinerCloseDeadlock(t *testing.T) {
	mt, err := createMinerTester(t.Name())
	if err != nil {
		t.Fatal(err)
	}
	// StartCPUMining calls `go threadedMine()`, which needs to access the miner
	// before Close() does in the next goroutine, otherwise m.tg.Add() fails
	// at the top of threadedMine() and threadedMine() exits (silently!).
	// I haven't seen this behavior since sticking Close() inside a goroutine,
	// but I'm not sure that's comfort enough.
	mt.miner.StartCPUMining()
	time.Sleep(time.Millisecond * 250)

	closed := make(chan struct{})
	go func() {
		if err := mt.miner.Close(); err != nil {
			t.Fatal(err)
		}
		closed <- struct{}{}
	}()
	select {
	case <-closed:
	case <-time.After(5 * time.Second):
		t.Fatal("mt.miner.Close never completed")
	}
}