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