whitebox_test.go raw
1 package ffldb
2
3 // This file is part of the ffldb package rather than the ffldb_test package as it provides whitebox testing.
4 import (
5 "compress/bzip2"
6 "encoding/binary"
7 "fmt"
8 "github.com/p9c/p9/pkg/block"
9 "hash/crc32"
10 "io"
11 "os"
12 "path/filepath"
13 "testing"
14
15 "github.com/btcsuite/goleveldb/leveldb"
16 ldberrors "github.com/btcsuite/goleveldb/leveldb/errors"
17
18 "github.com/p9c/p9/pkg/chaincfg"
19 "github.com/p9c/p9/pkg/database"
20 "github.com/p9c/p9/pkg/wire"
21 )
22
23 var (
24 // blockDataNet is the expected network in the test block data.
25 blockDataNet = wire.MainNet
26 // blockDataFile is the path to a file containing the first 256 blocks of the block chain.
27 blockDataFile = filepath.Join("..", "tstdata", "blocks1-256.bz2")
28 // errSubTestFail is used to signal that a sub test returned false.
29 errSubTestFail = fmt.Errorf("sub test failure")
30 )
31
32 // loadBlocks loads the blocks contained in the tstdata directory and returns a slice of them.
33 func loadBlocks(t *testing.T, dataFile string, network wire.BitcoinNet) ([]*block.Block, error) {
34 // Open the file that contains the blocks for reading.
35 fi, e := os.Open(dataFile)
36 if e != nil {
37 t.Errorf("failed to open file %v, err %v", dataFile, e)
38 return nil, e
39 }
40 defer func() {
41 if e := fi.Close(); E.Chk(e) {
42 t.Errorf(
43 "failed to close file %v %v", dataFile,
44 e,
45 )
46 }
47 }()
48 dr := bzip2.NewReader(fi)
49 // Set the first block as the genesis block.
50 blocks := make([]*block.Block, 0, 256)
51 genesis := block.NewBlock(chaincfg.MainNetParams.GenesisBlock)
52 blocks = append(blocks, genesis)
53 // Load the remaining blocks.
54 for height := 1; ; height++ {
55 var net uint32
56 e := binary.Read(dr, binary.LittleEndian, &net)
57 if e == io.EOF {
58 // Hit end of file at the expected offset. No error.
59 break
60 }
61 if e != nil {
62 t.Errorf(
63 "Failed to load network type for block %d: %v",
64 height, e,
65 )
66 return nil, e
67 }
68 if net != uint32(network) {
69 t.Errorf(
70 "Block doesn't match network: %v expects %v",
71 net, network,
72 )
73 return nil, e
74 }
75 var blockLen uint32
76 e = binary.Read(dr, binary.LittleEndian, &blockLen)
77 if e != nil {
78 t.Errorf(
79 "Failed to load block size for block %d: %v",
80 height, e,
81 )
82 return nil, e
83 }
84 // Read the block.
85 blockBytes := make([]byte, blockLen)
86 _, e = io.ReadFull(dr, blockBytes)
87 if e != nil {
88 t.Errorf("Failed to load block %d: %v", height, e)
89 return nil, e
90 }
91 // Deserialize and store the block.
92 block, e := block.NewFromBytes(blockBytes)
93 if e != nil {
94 t.Errorf("Failed to parse block %v: %v", height, e)
95 return nil, e
96 }
97 blocks = append(blocks, block)
98 }
99 return blocks, nil
100 }
101
102 // checkDbError ensures the passed error is a database.DBError with an error code that matches the passed error code.
103 func checkDbError(t *testing.T, testName string, gotErr error, wantErrCode database.ErrorCode) bool {
104 dbErr, ok := gotErr.(database.DBError)
105 if !ok {
106 t.Errorf(
107 "%s: unexpected error type - got %T, want %T",
108 testName, gotErr, database.DBError{},
109 )
110 return false
111 }
112 if dbErr.ErrorCode != wantErrCode {
113 t.Errorf(
114 "%s: unexpected error code - got %s (%s), want %s",
115 testName, dbErr.ErrorCode, dbErr.Description,
116 wantErrCode,
117 )
118 return false
119 }
120 return true
121 }
122
123 // testContext is used to store context information about a running test which is passed into helper functions.
124 type testContext struct {
125 t *testing.T
126 db database.DB
127 files map[uint32]*lockableFile
128 maxFileSizes map[uint32]int64
129 blocks []*block.Block
130 }
131
132 // TestConvertErr ensures the leveldb error to database error conversion works as expected.
133 func TestConvertErr(t *testing.T) {
134 t.Parallel()
135 tests := []struct {
136 err error
137 wantErrCode database.ErrorCode
138 }{
139 {&ldberrors.ErrCorrupted{}, database.ErrCorruption},
140 {leveldb.ErrClosed, database.ErrDbNotOpen},
141 {leveldb.ErrSnapshotReleased, database.ErrTxClosed},
142 {leveldb.ErrIterReleased, database.ErrTxClosed},
143 }
144 for i, test := range tests {
145 gotErr := convertErr("test", test.err)
146 if gotErr.ErrorCode != test.wantErrCode {
147 t.Errorf("convertErr #%d unexpected error - got %v, want %v", i, gotErr.ErrorCode, test.wantErrCode)
148 continue
149 }
150 }
151 }
152
153 // TestCornerCases ensures several corner cases which can happen when opening a database and/or block files work as expected.
154 func TestCornerCases(t *testing.T) {
155 t.Parallel()
156 // Create a file at the datapase path to force the open below to fail.
157 dbPath := filepath.Join(os.TempDir(), "ffldb-errors")
158 _ = os.RemoveAll(dbPath)
159 fi, e := os.Create(dbPath)
160 if e != nil {
161 t.Errorf("os.Create: unexpected error: %v", e)
162 return
163 }
164 if e = fi.Close(); E.Chk(e) {
165 }
166 // Ensure creating a new database fails when a file exists where a directory is needed.
167 testName := "openDB: fail due to file at target location"
168 wantErrCode := database.ErrDriverSpecific
169 var idb database.DB
170 if idb, e = openDB(dbPath, blockDataNet, true); E.Chk(e) {
171 }
172 if !checkDbError(t, testName, e, wantErrCode) {
173 if e = idb.Close(); E.Chk(e) {
174 }
175 if e = os.RemoveAll(dbPath); E.Chk(e) {
176 }
177 return
178 }
179 // Remove the file and create the database to run tests against. It should be successful this time.
180 _ = os.RemoveAll(dbPath)
181 idb, e = openDB(dbPath, blockDataNet, true)
182 if e != nil {
183 t.Errorf("openDB: unexpected error: %v", e)
184 return
185 }
186 defer func() {
187 if e = os.RemoveAll(dbPath); E.Chk(e) {
188 }
189 if e = idb.Close(); E.Chk(e) {
190 }
191 }()
192 // Ensure attempting to write to a file that can't be created returns the expected error.
193 testName = "writeBlock: open file failure"
194 filePath := blockFilePath(dbPath, 0)
195 if e = os.Mkdir(filePath, 0755); E.Chk(e) {
196 t.Errorf("os.Mkdir: unexpected error: %v", e)
197 return
198 }
199 store := idb.(*db).store
200 _, e = store.writeBlock([]byte{0x00})
201 if !checkDbError(t, testName, e, database.ErrDriverSpecific) {
202 return
203 }
204 _ = os.RemoveAll(filePath)
205 // Close the underlying leveldb database out from under the database.
206 ldb := idb.(*db).cache.ldb
207 if e = ldb.Close(); E.Chk(e) {
208 }
209 // Ensure initilization errors in the underlying database work as expected.
210 testName = "initDB: reinitialization"
211 wantErrCode = database.ErrDbNotOpen
212 e = initDB(ldb)
213 if !checkDbError(t, testName, e, wantErrCode) {
214 return
215 }
216 // Ensure the View handles errors in the underlying leveldb database properly.
217 testName = "View: underlying leveldb error"
218 wantErrCode = database.ErrDbNotOpen
219 e = idb.View(
220 func(tx database.Tx) (e error) {
221 return nil
222 },
223 )
224 if !checkDbError(t, testName, e, wantErrCode) {
225 return
226 }
227 // Ensure the Update handles errors in the underlying leveldb database properly.
228 testName = "Update: underlying leveldb error"
229 e = idb.Update(
230 func(tx database.Tx) (e error) {
231 return nil
232 },
233 )
234 if !checkDbError(t, testName, e, wantErrCode) {
235 return
236 }
237 }
238
239 // resetDatabase removes everything from the opened database associated with the test context including all metadata and the mock files.
240 func resetDatabase(tc *testContext) bool {
241 // Reset the metadata.
242 e := tc.db.Update(
243 func(tx database.Tx) (e error) {
244 // Remove all the keys using a cursor while also generating a list of buckets. It's not safe to remove keys during ForEach iteration nor is it safe to remove buckets during cursor iteration, so this dual approach is needed.
245 var bucketNames [][]byte
246 cursor := tx.Metadata().Cursor()
247 for ok := cursor.First(); ok; ok = cursor.Next() {
248 if cursor.Value() != nil {
249 if e = cursor.Delete(); E.Chk(e) {
250 return e
251 }
252 } else {
253 bucketNames = append(bucketNames, cursor.Key())
254 }
255 }
256 // Remove the buckets.
257 for _, k := range bucketNames {
258 if e = tx.Metadata().DeleteBucket(k); E.Chk(e) {
259 return e
260 }
261 }
262 _, e = tx.Metadata().CreateBucket(blockIdxBucketName)
263 return e
264 },
265 )
266 if e != nil {
267 tc.t.Errorf("Update: unexpected error: %v", e)
268 return false
269 }
270 // Reset the mock files.
271 store := tc.db.(*db).store
272 wc := store.writeCursor
273 wc.curFile.Lock()
274 if wc.curFile.file != nil {
275 if e := wc.curFile.file.Close(); E.Chk(e) {
276 }
277 wc.curFile.file = nil
278 }
279 wc.curFile.Unlock()
280 wc.Lock()
281 wc.curFileNum = 0
282 wc.curOffset = 0
283 wc.Unlock()
284 tc.files = make(map[uint32]*lockableFile)
285 tc.maxFileSizes = make(map[uint32]int64)
286 return true
287 }
288
289 // testWriteFailures tests various failures paths when writing to the block files.
290 func testWriteFailures(tc *testContext) bool {
291 if !resetDatabase(tc) {
292 return false
293 }
294 // Ensure file sync errors during flush return the expected error.
295 store := tc.db.(*db).store
296 testName := "flush: file sync failure"
297 store.writeCursor.Lock()
298 oldFile := store.writeCursor.curFile
299 store.writeCursor.curFile = &lockableFile{
300 file: &mockFile{forceSyncErr: true, maxSize: -1},
301 }
302 store.writeCursor.Unlock()
303 var e error
304 e = tc.db.(*db).cache.flush()
305 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
306 return false
307 }
308 store.writeCursor.Lock()
309 store.writeCursor.curFile = oldFile
310 store.writeCursor.Unlock()
311 // Force errors in the various error paths when writing data by using mock files with a limited max size.
312 block0Bytes, _ := tc.blocks[0].Bytes()
313 tests := []struct {
314 fileNum uint32
315 maxSize int64
316 }{
317 // Force an error when writing the network bytes.
318 {fileNum: 0, maxSize: 2},
319 // Force an error when writing the block size.
320 {fileNum: 0, maxSize: 6},
321 // Force an error when writing the block.
322 {fileNum: 0, maxSize: 17},
323 // Force an error when writing the checksum.
324 {fileNum: 0, maxSize: int64(len(block0Bytes)) + 10},
325 // Force an error after writing enough blocks for force multiple
326 // files.
327 {fileNum: 15, maxSize: 1},
328 }
329 for i, test := range tests {
330 if !resetDatabase(tc) {
331 return false
332 }
333 // Ensure storing the specified number of blocks using a mock file that fails the write fails when the transaction is committed, not when the block is stored.
334 tc.maxFileSizes = map[uint32]int64{test.fileNum: test.maxSize}
335 e = tc.db.Update(
336 func(tx database.Tx) (e error) {
337 for i, block := range tc.blocks {
338 e := tx.StoreBlock(block)
339 if e != nil {
340 tc.t.Errorf(
341 "StoreBlock (%d): unexpected "+
342 "error: %v", i, e,
343 )
344 return errSubTestFail
345 }
346 }
347 return nil
348 },
349 )
350 testName := fmt.Sprintf(
351 "Force update commit failure - test "+
352 "%d, fileNum %d, maxsize %d", i, test.fileNum,
353 test.maxSize,
354 )
355 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
356 tc.t.Errorf("%v", e)
357 return false
358 }
359 // Ensure the commit rollback removed all extra files and data.
360 if len(tc.files) != 1 {
361 tc.t.Errorf(
362 "Update rollback: new not removed - want "+
363 "1 file, got %d", len(tc.files),
364 )
365 return false
366 }
367 if _, ok := tc.files[0]; !ok {
368 tc.t.Error("Update rollback: file 0 does not exist")
369 return false
370 }
371 file := tc.files[0].file.(*mockFile)
372 if len(file.data) != 0 {
373 tc.t.Errorf(
374 "Update rollback: file did not truncate - "+
375 "want len 0, got len %d", len(file.data),
376 )
377 return false
378 }
379 }
380 return true
381 }
382
383 // testBlockFileErrors ensures the database returns expected errors with various file-related issues such as closed and missing files.
384 func testBlockFileErrors(tc *testContext) bool {
385 if !resetDatabase(tc) {
386 return false
387 }
388 // Ensure errors in blockFile and openFile when requesting invalid file numbers.
389 store := tc.db.(*db).store
390 testName := "blockFile invalid file open"
391 _, e := store.blockFile(^uint32(0))
392 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
393 return false
394 }
395 testName = "openFile invalid file open"
396 _, e = store.openFile(^uint32(0))
397 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
398 return false
399 }
400 // Insert the first block into the mock file.
401 e = tc.db.Update(
402 func(tx database.Tx) (e error) {
403 e = tx.StoreBlock(tc.blocks[0])
404 if e != nil {
405 tc.t.Errorf("StoreBlock: unexpected error: %v", e)
406 return errSubTestFail
407 }
408 return nil
409 },
410 )
411 if e != nil {
412 if e != errSubTestFail {
413 tc.t.Errorf("Update: unexpected error: %v", e)
414 }
415 return false
416 }
417 // Ensure errors in readBlock and readBlockRegion when requesting a file number that doesn't exist.
418 block0Hash := tc.blocks[0].Hash()
419 testName = "readBlock invalid file number"
420 invalidLoc := blockLocation{
421 blockFileNum: ^uint32(0),
422 blockLen: 80,
423 }
424 _, e = store.readBlock(block0Hash, invalidLoc)
425 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
426 return false
427 }
428 testName = "readBlockRegion invalid file number"
429 _, e = store.readBlockRegion(invalidLoc, 0, 80)
430 if !checkDbError(tc.t, testName, e, database.ErrDriverSpecific) {
431 return false
432 }
433 // Close the block file out from under the database.
434 store.writeCursor.curFile.Lock()
435 if e = store.writeCursor.curFile.file.Close(); E.Chk(e) {
436 }
437 store.writeCursor.curFile.Unlock()
438 // Ensure failures in FetchBlock and FetchBlockRegion(s) since the underlying file they need to read from has been closed.
439 e = tc.db.View(
440 func(tx database.Tx) (e error) {
441 testName = "FetchBlock closed file"
442 wantErrCode := database.ErrDriverSpecific
443 _, e = tx.FetchBlock(block0Hash)
444 if !checkDbError(tc.t, testName, e, wantErrCode) {
445 return errSubTestFail
446 }
447 testName = "FetchBlockRegion closed file"
448 regions := []database.BlockRegion{
449 {
450 Hash: block0Hash,
451 Len: 80,
452 Offset: 0,
453 },
454 }
455 _, e = tx.FetchBlockRegion(®ions[0])
456 if !checkDbError(tc.t, testName, e, wantErrCode) {
457 return errSubTestFail
458 }
459 testName = "FetchBlockRegions closed file"
460 _, e = tx.FetchBlockRegions(regions)
461 if !checkDbError(tc.t, testName, e, wantErrCode) {
462 return errSubTestFail
463 }
464 return nil
465 },
466 )
467 if e != nil {
468 if e != errSubTestFail {
469 tc.t.Errorf("View: unexpected error: %v", e)
470 }
471 return false
472 }
473 return true
474 }
475
476 // testCorruption ensures the database returns expected errors under various corruption scenarios.
477 func testCorruption(tc *testContext) bool {
478 if !resetDatabase(tc) {
479 return false
480 }
481 // Insert the first block into the mock file.
482 e := tc.db.Update(
483 func(tx database.Tx) (e error) {
484 e = tx.StoreBlock(tc.blocks[0])
485 if e != nil {
486 tc.t.Errorf("StoreBlock: unexpected error: %v", e)
487 return errSubTestFail
488 }
489 return nil
490 },
491 )
492 if e != nil {
493 if e != errSubTestFail {
494 tc.t.Errorf("Update: unexpected error: %v", e)
495 }
496 return false
497 }
498 // Ensure corruption is detected by intentionally modifying the bytes stored to the mock file and reading the block.
499 block0Bytes, _ := tc.blocks[0].Bytes()
500 block0Hash := tc.blocks[0].Hash()
501 tests := []struct {
502 offset uint32
503 fixChecksum bool
504 wantErrCode database.ErrorCode
505 }{
506 // One of the network bytes. The checksum needs to be fixed so the invalid network is detected.
507 {2, true, database.ErrDriverSpecific},
508 // The same network byte, but this time don't fix the checksum to ensure the corruption is detected.
509 {2, false, database.ErrCorruption},
510 // One of the block length bytes.
511 {6, false, database.ErrCorruption},
512 // Random header byte.
513 {17, false, database.ErrCorruption},
514 // Random transaction byte.
515 {90, false, database.ErrCorruption},
516 // Random checksum byte.
517 {uint32(len(block0Bytes)) + 10, false, database.ErrCorruption},
518 }
519 e = tc.db.View(
520 func(tx database.Tx) (e error) {
521 data := tc.files[0].file.(*mockFile).data
522 for i, test := range tests {
523 // Corrupt the byte at the offset by a single bit.
524 data[test.offset] ^= 0x10
525 // Fix the checksum if requested to force other errors.
526 fileLen := len(data)
527 var oldChecksumBytes [4]byte
528 copy(oldChecksumBytes[:], data[fileLen-4:])
529 if test.fixChecksum {
530 toSum := data[:fileLen-4]
531 cksum := crc32.Checksum(toSum, castagnoli)
532 binary.BigEndian.PutUint32(data[fileLen-4:], cksum)
533 }
534 testName := fmt.Sprintf(
535 "FetchBlock (test #%d): "+
536 "corruption", i,
537 )
538 _, e = tx.FetchBlock(block0Hash)
539 if !checkDbError(tc.t, testName, e, test.wantErrCode) {
540 return errSubTestFail
541 }
542 // Reset the corrupted data back to the original.
543 data[test.offset] ^= 0x10
544 if test.fixChecksum {
545 copy(data[fileLen-4:], oldChecksumBytes[:])
546 }
547 }
548 return nil
549 },
550 )
551 if e != nil {
552 if e != errSubTestFail {
553 tc.t.Errorf("View: unexpected error: %v", e)
554 }
555 return false
556 }
557 return true
558 }
559
560 // // TestFailureScenarios ensures several failure scenarios such as database corruption, block file write failures, and rollback failures are handled correctly.
561 // func TestFailureScenarios(// t *testing.T) {
562 // // Create a new database to run tests against.
563 // dbPath := filepath.Join(os.TempDir(), "ffldb-failurescenarios")
564 // _ = os.RemoveAll(dbPath)
565 // idb, e := database.Create(dbType, dbPath, blockDataNet)
566 // if e != nil {
567 // t.Errorf("Failed to create test database (%s) %v", dbType, err)
568 // return
569 // }
570 // defer os.RemoveAll(dbPath)
571 // defer idb.Close()
572 // // Create a test context to pass around.
573 // tc := &testContext{
574 // t: t,
575 // db: idb,
576 // files: make(map[uint32]*lockableFile),
577 // maxFileSizes: make(map[uint32]int64),
578 // }
579 // // Change the maximum file size to a small value to force multiple flat files with the test data set and replace the file-related functions to make use of mock files in memory. This allows injection of various file-related errors.
580 // store := idb.(*db).store
581 // store.maxBlockFileSize = 1024 // 1KiB
582 // store.openWriteFileFunc = func(fileNum uint32) (filer, error) {
583 // if file, ok := tc.files[fileNum]; ok {
584 // // "Reopen" the file.
585 // file.Lock()
586 // mock := file.file.(*mockFile)
587 // mock.Lock()
588 // mock.closed = false
589 // mock.Unlock()
590 // file.Unlock()
591 // return mock, nil
592 // }
593 // // Limit the max size of the mock file as specified in the test context.
594 // maxSize := int64(-1)
595 // if maxFileSize, ok := tc.maxFileSizes[fileNum]; ok {
596 // maxSize = int64(maxFileSize)
597 // }
598 // file := &mockFile{maxSize: int64(maxSize)}
599 // tc.files[fileNum] = &lockableFile{file: file}
600 // return file, nil
601 // }
602 // store.openFileFunc = func(fileNum uint32) (*lockableFile, error) {
603 // // Force error when trying to open max file num.
604 // if fileNum == ^uint32(0) {
605 // return nil, makeDbErr(database.ErrDriverSpecific,
606 // "test", nil)
607 // }
608 // if file, ok := tc.files[fileNum]; ok {
609 // // "Reopen" the file.
610 // file.Lock()
611 // mock := file.file.(*mockFile)
612 // mock.Lock()
613 // mock.closed = false
614 // mock.Unlock()
615 // file.Unlock()
616 // return file, nil
617 // }
618 // file := &lockableFile{file: &mockFile{}}
619 // tc.files[fileNum] = file
620 // return file, nil
621 // }
622 // store.deleteFileFunc = func(fileNum uint32) (e error) {
623 // if file, ok := tc.files[fileNum]; ok {
624 // file.Lock()
625 // file.file.Close()
626 // file.Unlock()
627 // delete(tc.files, fileNum)
628 // return nil
629 // }
630 // str := fmt.Sprintf("file %d does not exist", fileNum)
631 // return makeDbErr(database.ErrDriverSpecific, str, nil)
632 // }
633 // // Load the test blocks and save in the test context for use throughout the tests.
634 // blocks, e := loadBlocks(t, blockDataFile, blockDataNet)
635 // if e != nil {
636 // t.Errorf("loadBlocks: Unexpected error: %v", err)
637 // return
638 // }
639 // tc.blocks = blocks
640 // // Test various failures paths when writing to the block files.
641 // if !testWriteFailures(tc) {
642 // return
643 // }
644 // // Test various file-related issues such as closed and missing files.
645 // if !testBlockFileErrors(tc) {
646 // return
647 // }
648 // // Test various corruption scenarios.
649 // testCorruption(tc)
650 // }
651