github.com/btcsuite/btcd@v0.24.0/blockchain/utxocache_test.go (about) 1 // Copyright (c) 2023 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 package blockchain 5 6 import ( 7 "crypto/sha256" 8 "encoding/binary" 9 "fmt" 10 "path/filepath" 11 "reflect" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/btcsuite/btcd/btcutil" 17 "github.com/btcsuite/btcd/chaincfg" 18 "github.com/btcsuite/btcd/chaincfg/chainhash" 19 "github.com/btcsuite/btcd/database" 20 "github.com/btcsuite/btcd/database/ffldb" 21 "github.com/btcsuite/btcd/wire" 22 ) 23 24 func TestMapSlice(t *testing.T) { 25 tests := []struct { 26 keys []wire.OutPoint 27 }{ 28 { 29 keys: func() []wire.OutPoint { 30 outPoints := make([]wire.OutPoint, 1000) 31 for i := uint32(0); i < uint32(len(outPoints)); i++ { 32 var buf [4]byte 33 binary.BigEndian.PutUint32(buf[:], i) 34 hash := sha256.Sum256(buf[:]) 35 36 op := wire.OutPoint{Hash: hash, Index: i} 37 outPoints[i] = op 38 } 39 return outPoints 40 }(), 41 }, 42 } 43 44 for _, test := range tests { 45 m := make(map[wire.OutPoint]*UtxoEntry) 46 47 maxSize := calculateRoughMapSize(1000, bucketSize) 48 49 maxEntriesFirstMap := 500 50 ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap) 51 ms := mapSlice{ 52 maps: []map[wire.OutPoint]*UtxoEntry{ms1}, 53 maxEntries: []int{maxEntriesFirstMap}, 54 maxTotalMemoryUsage: uint64(maxSize), 55 } 56 57 for _, key := range test.keys { 58 m[key] = nil 59 ms.put(key, nil, 0) 60 } 61 62 // Put in the same elements twice to test that the map slice won't hold duplicates. 63 for _, key := range test.keys { 64 m[key] = nil 65 ms.put(key, nil, 0) 66 } 67 68 if len(m) != ms.length() { 69 t.Fatalf("expected len of %d, got %d", len(m), ms.length()) 70 } 71 72 for _, key := range test.keys { 73 expected, found := m[key] 74 if !found { 75 t.Fatalf("expected key %s to exist in the go map", key.String()) 76 } 77 78 got, found := ms.get(key) 79 if !found { 80 t.Fatalf("expected key %s to exist in the map slice", key.String()) 81 } 82 83 if !reflect.DeepEqual(got, expected) { 84 t.Fatalf("expected value of %v, got %v", expected, got) 85 } 86 } 87 } 88 } 89 90 // TestMapsliceConcurrency just tests that the mapslice won't result in a panic 91 // on concurrent access. 92 func TestMapsliceConcurrency(t *testing.T) { 93 tests := []struct { 94 keys []wire.OutPoint 95 }{ 96 { 97 keys: func() []wire.OutPoint { 98 outPoints := make([]wire.OutPoint, 10000) 99 for i := uint32(0); i < uint32(len(outPoints)); i++ { 100 var buf [4]byte 101 binary.BigEndian.PutUint32(buf[:], i) 102 hash := sha256.Sum256(buf[:]) 103 104 op := wire.OutPoint{Hash: hash, Index: i} 105 outPoints[i] = op 106 } 107 return outPoints 108 }(), 109 }, 110 } 111 112 for _, test := range tests { 113 maxSize := calculateRoughMapSize(1000, bucketSize) 114 115 maxEntriesFirstMap := 500 116 ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap) 117 ms := mapSlice{ 118 maps: []map[wire.OutPoint]*UtxoEntry{ms1}, 119 maxEntries: []int{maxEntriesFirstMap}, 120 maxTotalMemoryUsage: uint64(maxSize), 121 } 122 123 var wg sync.WaitGroup 124 125 wg.Add(1) 126 go func(m *mapSlice, keys []wire.OutPoint) { 127 defer wg.Done() 128 for i := 0; i < 5000; i++ { 129 m.put(keys[i], nil, 0) 130 } 131 }(&ms, test.keys) 132 133 wg.Add(1) 134 go func(m *mapSlice, keys []wire.OutPoint) { 135 defer wg.Done() 136 for i := 5000; i < 10000; i++ { 137 m.put(keys[i], nil, 0) 138 } 139 }(&ms, test.keys) 140 141 wg.Add(1) 142 go func(m *mapSlice) { 143 defer wg.Done() 144 for i := 0; i < 10000; i++ { 145 m.size() 146 } 147 }(&ms) 148 149 wg.Add(1) 150 go func(m *mapSlice) { 151 defer wg.Done() 152 for i := 0; i < 10000; i++ { 153 m.length() 154 } 155 }(&ms) 156 157 wg.Add(1) 158 go func(m *mapSlice, keys []wire.OutPoint) { 159 defer wg.Done() 160 for i := 0; i < 10000; i++ { 161 m.get(keys[i]) 162 } 163 }(&ms, test.keys) 164 165 wg.Add(1) 166 go func(m *mapSlice, keys []wire.OutPoint) { 167 defer wg.Done() 168 for i := 0; i < 5000; i++ { 169 m.delete(keys[i]) 170 } 171 }(&ms, test.keys) 172 173 wg.Wait() 174 } 175 } 176 177 // getValidP2PKHScript returns a valid P2PKH script. Useful as unspendables cannot be 178 // added to the cache. 179 func getValidP2PKHScript() []byte { 180 validP2PKHScript := []byte{ 181 // OP_DUP 182 0x76, 183 // OP_HASH160 184 0xa9, 185 // OP_DATA_20 186 0x14, 187 // <20-byte pubkey hash> 188 0xf0, 0x7a, 0xb8, 0xce, 0x72, 0xda, 0x4e, 0x76, 189 0x0b, 0x74, 0x7d, 0x48, 0xd6, 0x65, 0xec, 0x96, 190 0xad, 0xf0, 0x24, 0xf5, 191 // OP_EQUALVERIFY 192 0x88, 193 // OP_CHECKSIG 194 0xac, 195 } 196 return validP2PKHScript 197 } 198 199 // outpointFromInt generates an outpoint from an int by hashing the int and making 200 // the given int the index. 201 func outpointFromInt(i int) wire.OutPoint { 202 // Boilerplate to create an outpoint. 203 var buf [4]byte 204 binary.BigEndian.PutUint32(buf[:], uint32(i)) 205 hash := sha256.Sum256(buf[:]) 206 return wire.OutPoint{Hash: hash, Index: uint32(i)} 207 } 208 209 func TestUtxoCacheEntrySize(t *testing.T) { 210 type block struct { 211 txOuts []*wire.TxOut 212 outOps []wire.OutPoint 213 txIns []*wire.TxIn 214 } 215 tests := []struct { 216 name string 217 blocks []block 218 expectedSize uint64 219 }{ 220 { 221 name: "one entry", 222 blocks: func() []block { 223 return []block{ 224 { 225 txOuts: []*wire.TxOut{ 226 {Value: 10000, PkScript: getValidP2PKHScript()}, 227 }, 228 outOps: []wire.OutPoint{ 229 outpointFromInt(0), 230 }, 231 }, 232 } 233 }(), 234 expectedSize: pubKeyHashLen + baseEntrySize, 235 }, 236 { 237 name: "10 entries, 4 spend", 238 blocks: func() []block { 239 blocks := make([]block, 0, 10) 240 for i := 0; i < 10; i++ { 241 op := outpointFromInt(i) 242 243 block := block{ 244 txOuts: []*wire.TxOut{ 245 {Value: 10000, PkScript: getValidP2PKHScript()}, 246 }, 247 outOps: []wire.OutPoint{ 248 op, 249 }, 250 } 251 252 // Spend all outs in blocks less than 4. 253 if i < 4 { 254 block.txIns = []*wire.TxIn{ 255 {PreviousOutPoint: op}, 256 } 257 } 258 259 blocks = append(blocks, block) 260 } 261 return blocks 262 }(), 263 // Multipled by 6 since we'll have 6 entries left. 264 expectedSize: (pubKeyHashLen + baseEntrySize) * 6, 265 }, 266 { 267 name: "spend everything", 268 blocks: func() []block { 269 blocks := make([]block, 0, 500) 270 for i := 0; i < 500; i++ { 271 op := outpointFromInt(i) 272 273 block := block{ 274 txOuts: []*wire.TxOut{ 275 {Value: 1000, PkScript: getValidP2PKHScript()}, 276 }, 277 outOps: []wire.OutPoint{ 278 op, 279 }, 280 } 281 282 // Spend all outs in blocks less than 4. 283 block.txIns = []*wire.TxIn{ 284 {PreviousOutPoint: op}, 285 } 286 287 blocks = append(blocks, block) 288 } 289 return blocks 290 }(), 291 expectedSize: 0, 292 }, 293 } 294 295 for _, test := range tests { 296 // Size is just something big enough so that the mapslice doesn't 297 // run out of memory. 298 s := newUtxoCache(nil, 1*1024*1024) 299 300 for height, block := range test.blocks { 301 for i, out := range block.txOuts { 302 s.addTxOut(block.outOps[i], out, true, int32(height)) 303 } 304 305 for _, in := range block.txIns { 306 s.addTxIn(in, nil) 307 } 308 } 309 310 if s.totalEntryMemory != test.expectedSize { 311 t.Errorf("Failed test %s. Expected size of %d, got %d", 312 test.name, test.expectedSize, s.totalEntryMemory) 313 } 314 } 315 } 316 317 // assertConsistencyState asserts the utxo consistency states of the blockchain. 318 func assertConsistencyState(chain *BlockChain, hash *chainhash.Hash) error { 319 var bytes []byte 320 err := chain.db.View(func(dbTx database.Tx) (err error) { 321 bytes = dbFetchUtxoStateConsistency(dbTx) 322 return 323 }) 324 if err != nil { 325 return fmt.Errorf("Error fetching utxo state consistency: %v", err) 326 } 327 actualHash, err := chainhash.NewHash(bytes) 328 if err != nil { 329 return err 330 } 331 if !actualHash.IsEqual(hash) { 332 return fmt.Errorf("Unexpected consistency hash: %v instead of %v", 333 actualHash, hash) 334 } 335 336 return nil 337 } 338 339 // assertNbEntriesOnDisk asserts that the total number of utxo entries on the 340 // disk is equal to the given expected number. 341 func assertNbEntriesOnDisk(chain *BlockChain, expectedNumber int) error { 342 var nb int 343 err := chain.db.View(func(dbTx database.Tx) error { 344 cursor := dbTx.Metadata().Bucket(utxoSetBucketName).Cursor() 345 nb = 0 346 for b := cursor.First(); b; b = cursor.Next() { 347 nb++ 348 _, err := deserializeUtxoEntry(cursor.Value()) 349 if err != nil { 350 return fmt.Errorf("Failed to deserialize entry: %v", err) 351 } 352 } 353 return nil 354 }) 355 if err != nil { 356 return fmt.Errorf("Error fetching utxo entries: %v", err) 357 } 358 if nb != expectedNumber { 359 return fmt.Errorf("Expected %d elements in the UTXO set, but found %d", 360 expectedNumber, nb) 361 } 362 363 return nil 364 } 365 366 // utxoCacheTestChain creates a test BlockChain to be used for utxo cache tests. 367 // It uses the regression test parameters, a coin matutiry of 1 block and sets 368 // the cache size limit to 10 MiB. 369 func utxoCacheTestChain(testName string) (*BlockChain, *chaincfg.Params, func()) { 370 params := chaincfg.RegressionNetParams 371 chain, tearDown, err := chainSetup(testName, ¶ms) 372 if err != nil { 373 panic(fmt.Sprintf("error loading blockchain with database: %v", err)) 374 } 375 376 chain.TstSetCoinbaseMaturity(1) 377 chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 378 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage 379 380 return chain, ¶ms, tearDown 381 } 382 383 func TestUtxoCacheFlush(t *testing.T) { 384 chain, params, tearDown := utxoCacheTestChain("TestUtxoCacheFlush") 385 defer tearDown() 386 cache := chain.utxoCache 387 tip := btcutil.NewBlock(params.GenesisBlock) 388 389 // The chainSetup init triggers the consistency status write. 390 err := assertConsistencyState(chain, params.GenesisHash) 391 if err != nil { 392 t.Fatal(err) 393 } 394 395 err = assertNbEntriesOnDisk(chain, 0) 396 if err != nil { 397 t.Fatal(err) 398 } 399 400 // LastFlushHash starts with genesis. 401 if cache.lastFlushHash != *params.GenesisHash { 402 t.Fatalf("lastFlushHash before first flush expected to be "+ 403 "genesis block hash, instead was %v", cache.lastFlushHash) 404 } 405 406 // First, add 10 utxos without flushing. 407 outPoints := make([]wire.OutPoint, 10) 408 for i := range outPoints { 409 op := outpointFromInt(i) 410 outPoints[i] = op 411 412 // Add the txout. 413 txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()} 414 cache.addTxOut(op, &txOut, true, int32(i)) 415 } 416 417 if cache.cachedEntries.length() != len(outPoints) { 418 t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length()) 419 } 420 421 // All entries should be fresh and modified. 422 for _, m := range cache.cachedEntries.maps { 423 for outpoint, entry := range m { 424 if entry == nil { 425 t.Fatalf("Unexpected nil entry found for %v", outpoint) 426 } 427 if !entry.isModified() { 428 t.Fatal("Entry should be marked mofified") 429 } 430 if !entry.isFresh() { 431 t.Fatal("Entry should be marked fresh") 432 } 433 } 434 } 435 436 // Spend the last outpoint and pop it off from the outpoints slice. 437 var spendOp wire.OutPoint 438 spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1] 439 cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil) 440 441 if cache.cachedEntries.length() != len(outPoints) { 442 t.Fatalf("Expected %d entries, has %d instead", 443 len(outPoints), cache.cachedEntries.length()) 444 } 445 446 // Not flushed yet. 447 err = assertConsistencyState(chain, params.GenesisHash) 448 if err != nil { 449 t.Fatal(err) 450 } 451 452 err = assertNbEntriesOnDisk(chain, 0) 453 if err != nil { 454 t.Fatal(err) 455 } 456 457 // Flush. 458 err = chain.db.Update(func(dbTx database.Tx) error { 459 return cache.flush(dbTx, FlushRequired, chain.stateSnapshot) 460 }) 461 if err != nil { 462 t.Fatalf("unexpected error while flushing cache: %v", err) 463 } 464 if cache.cachedEntries.length() != 0 { 465 t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) 466 } 467 468 err = assertConsistencyState(chain, tip.Hash()) 469 if err != nil { 470 t.Fatal(err) 471 } 472 err = assertNbEntriesOnDisk(chain, len(outPoints)) 473 if err != nil { 474 t.Fatal(err) 475 } 476 477 // Fetch the flushed utxos. 478 entries, err := cache.fetchEntries(outPoints) 479 if err != nil { 480 t.Fatal(err) 481 } 482 483 // Check that the returned entries are not marked fresh and modified. 484 for _, entry := range entries { 485 if entry.isFresh() { 486 t.Fatal("Entry should not be marked fresh") 487 } 488 if entry.isModified() { 489 t.Fatal("Entry should not be marked modified") 490 } 491 } 492 493 // Check that the fetched entries in the cache are not marked fresh and modified. 494 for _, m := range cache.cachedEntries.maps { 495 for outpoint, elem := range m { 496 if elem == nil { 497 t.Fatalf("Unexpected nil entry found for %v", outpoint) 498 } 499 if elem.isFresh() { 500 t.Fatal("Entry should not be marked fresh") 501 } 502 if elem.isModified() { 503 t.Fatal("Entry should not be marked modified") 504 } 505 } 506 } 507 508 // Spend 5 utxos. 509 prevLen := len(outPoints) 510 for i := 0; i < 5; i++ { 511 spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1] 512 cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil) 513 } 514 515 // Should still have the entries in cache so they can be flushed to disk. 516 if cache.cachedEntries.length() != prevLen { 517 t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length()) 518 } 519 520 // Flush. 521 err = chain.db.Update(func(dbTx database.Tx) error { 522 return cache.flush(dbTx, FlushRequired, chain.stateSnapshot) 523 }) 524 if err != nil { 525 t.Fatalf("unexpected error while flushing cache: %v", err) 526 } 527 if cache.cachedEntries.length() != 0 { 528 t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) 529 } 530 531 err = assertConsistencyState(chain, tip.Hash()) 532 if err != nil { 533 t.Fatal(err) 534 } 535 err = assertNbEntriesOnDisk(chain, len(outPoints)) 536 if err != nil { 537 t.Fatal(err) 538 } 539 540 // Add 5 utxos without flushing and test for periodic flushes. 541 outPoints1 := make([]wire.OutPoint, 5) 542 for i := range outPoints1 { 543 // i + prevLen here to avoid collision since we're just hashing 544 // the int. 545 op := outpointFromInt(i + prevLen) 546 outPoints1[i] = op 547 548 // Add the txout. 549 txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()} 550 cache.addTxOut(op, &txOut, true, int32(i+prevLen)) 551 } 552 if cache.cachedEntries.length() != len(outPoints1) { 553 t.Fatalf("Expected %d entries, has %d instead", 554 len(outPoints1), cache.cachedEntries.length()) 555 } 556 557 // Attempt to flush with flush periodic. Shouldn't flush. 558 err = chain.db.Update(func(dbTx database.Tx) error { 559 return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot) 560 }) 561 if err != nil { 562 t.Fatalf("unexpected error while flushing cache: %v", err) 563 } 564 if cache.cachedEntries.length() == 0 { 565 t.Fatalf("Expected %d entries, has %d instead", 566 len(outPoints1), cache.cachedEntries.length()) 567 } 568 569 // Arbitrarily set the last flush time to 6 minutes ago. 570 cache.lastFlushTime = time.Now().Add(-time.Minute * 6) 571 572 // Attempt to flush with flush periodic. Should flush now. 573 err = chain.db.Update(func(dbTx database.Tx) error { 574 return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot) 575 }) 576 if err != nil { 577 t.Fatalf("unexpected error while flushing cache: %v", err) 578 } 579 if cache.cachedEntries.length() != 0 { 580 t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) 581 } 582 583 err = assertConsistencyState(chain, tip.Hash()) 584 if err != nil { 585 t.Fatal(err) 586 } 587 err = assertNbEntriesOnDisk(chain, len(outPoints)+len(outPoints1)) 588 if err != nil { 589 t.Fatal(err) 590 } 591 } 592 593 func TestFlushNeededAfterPrune(t *testing.T) { 594 // Construct a synthetic block chain with a block index consisting of 595 // the following structure. 596 // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 597 tip := tstTip 598 chain := newFakeChain(&chaincfg.MainNetParams) 599 chain.utxoCache = newUtxoCache(nil, 0) 600 branchNodes := chainedNodes(chain.bestChain.Genesis(), 18) 601 for _, node := range branchNodes { 602 chain.index.SetStatusFlags(node, statusValid) 603 chain.index.AddNode(node) 604 } 605 chain.bestChain.SetTip(tip(branchNodes)) 606 607 tests := []struct { 608 name string 609 lastFlushHash chainhash.Hash 610 delHashes []chainhash.Hash 611 expected bool 612 }{ 613 { 614 name: "deleted block up to height 9, last flush hash at block 10", 615 delHashes: func() []chainhash.Hash { 616 delBlockHashes := make([]chainhash.Hash, 0, 9) 617 for i := range branchNodes { 618 if branchNodes[i].height < 10 { 619 delBlockHashes = append(delBlockHashes, branchNodes[i].hash) 620 } 621 } 622 623 return delBlockHashes 624 }(), 625 lastFlushHash: func() chainhash.Hash { 626 // Just some sanity checking to make sure the height is 10. 627 if branchNodes[9].height != 10 { 628 panic("was looking for height 10") 629 } 630 return branchNodes[9].hash 631 }(), 632 expected: false, 633 }, 634 { 635 name: "deleted blocks up to height 10, last flush hash at block 10", 636 delHashes: func() []chainhash.Hash { 637 delBlockHashes := make([]chainhash.Hash, 0, 10) 638 for i := range branchNodes { 639 if branchNodes[i].height < 11 { 640 delBlockHashes = append(delBlockHashes, branchNodes[i].hash) 641 } 642 } 643 return delBlockHashes 644 }(), 645 lastFlushHash: func() chainhash.Hash { 646 // Just some sanity checking to make sure the height is 10. 647 if branchNodes[9].height != 10 { 648 panic("was looking for height 10") 649 } 650 return branchNodes[9].hash 651 }(), 652 expected: true, 653 }, 654 { 655 name: "deleted block height 17, last flush hash at block 5", 656 delHashes: func() []chainhash.Hash { 657 delBlockHashes := make([]chainhash.Hash, 1) 658 delBlockHashes[0] = branchNodes[16].hash 659 // Just some sanity checking to make sure the height is 10. 660 if branchNodes[16].height != 17 { 661 panic("was looking for height 17") 662 } 663 return delBlockHashes 664 }(), 665 lastFlushHash: func() chainhash.Hash { 666 // Just some sanity checking to make sure the height is 10. 667 if branchNodes[4].height != 5 { 668 panic("was looking for height 5") 669 } 670 return branchNodes[4].hash 671 }(), 672 expected: true, 673 }, 674 { 675 name: "deleted block height 3, last flush hash at block 4", 676 delHashes: func() []chainhash.Hash { 677 delBlockHashes := make([]chainhash.Hash, 1) 678 delBlockHashes[0] = branchNodes[2].hash 679 // Just some sanity checking to make sure the height is 10. 680 if branchNodes[2].height != 3 { 681 panic("was looking for height 3") 682 } 683 return delBlockHashes 684 }(), 685 lastFlushHash: func() chainhash.Hash { 686 // Just some sanity checking to make sure the height is 10. 687 if branchNodes[3].height != 4 { 688 panic("was looking for height 4") 689 } 690 return branchNodes[3].hash 691 }(), 692 expected: false, 693 }, 694 } 695 696 for _, test := range tests { 697 chain.utxoCache.lastFlushHash = test.lastFlushHash 698 got, err := chain.flushNeededAfterPrune(test.delHashes) 699 if err != nil { 700 t.Fatal(err) 701 } 702 703 if got != test.expected { 704 t.Fatalf("for test %s, expected need flush to return %v but got %v", 705 test.name, test.expected, got) 706 } 707 } 708 } 709 710 func TestFlushOnPrune(t *testing.T) { 711 chain, tearDown, err := chainSetup("TestFlushOnPrune", &chaincfg.MainNetParams) 712 if err != nil { 713 panic(fmt.Sprintf("error loading blockchain with database: %v", err)) 714 } 715 defer tearDown() 716 717 chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 718 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage 719 720 // Set the maxBlockFileSize and the prune target small so that we can trigger a 721 // prune to happen. 722 maxBlockFileSize := uint32(8192) 723 chain.pruneTarget = uint64(maxBlockFileSize) * 2 724 725 // Read blocks from the file. 726 blocks, err := loadBlocks("blk_0_to_14131.dat") 727 if err != nil { 728 t.Fatalf("failed to read block from file. %v", err) 729 } 730 731 syncBlocks := func() { 732 for i, block := range blocks { 733 if i == 0 { 734 // Skip the genesis block. 735 continue 736 } 737 isMainChain, _, err := chain.ProcessBlock(block, BFNone) 738 if err != nil { 739 t.Fatal(err) 740 } 741 742 if !isMainChain { 743 t.Fatalf("expected block %s to be on the main chain", block.Hash()) 744 } 745 } 746 } 747 748 // Sync the chain. 749 ffldb.TstRunWithMaxBlockFileSize(chain.db, maxBlockFileSize, syncBlocks) 750 751 // Function that errors out if the block that should exist doesn't exist. 752 shouldExist := func(dbTx database.Tx, blockHash *chainhash.Hash) { 753 bytes, err := dbTx.FetchBlock(blockHash) 754 if err != nil { 755 t.Fatal(err) 756 } 757 block, err := btcutil.NewBlockFromBytes(bytes) 758 if err != nil { 759 t.Fatalf("didn't find block %v. %v", blockHash, err) 760 } 761 762 if !block.Hash().IsEqual(blockHash) { 763 t.Fatalf("expected to find block %v but got %v", 764 blockHash, block.Hash()) 765 } 766 } 767 768 // Function that errors out if the block that shouldn't exist exists. 769 shouldNotExist := func(dbTx database.Tx, blockHash *chainhash.Hash) { 770 bytes, err := dbTx.FetchBlock(chaincfg.MainNetParams.GenesisHash) 771 if err == nil { 772 t.Fatalf("expected block %s to be pruned", blockHash) 773 } 774 if len(bytes) != 0 { 775 t.Fatalf("expected block %s to be pruned but got %v", 776 blockHash, bytes) 777 } 778 } 779 780 // The below code checks that the correct blocks were pruned. 781 chain.db.View(func(dbTx database.Tx) error { 782 exist := false 783 for _, block := range blocks { 784 // Blocks up to the last flush hash should not exist. 785 // The utxocache is big enough so that it shouldn't flush 786 // on it being full. It should only flush on prunes. 787 if block.Hash().IsEqual(&chain.utxoCache.lastFlushHash) { 788 exist = true 789 } 790 791 if exist { 792 shouldExist(dbTx, block.Hash()) 793 } else { 794 shouldNotExist(dbTx, block.Hash()) 795 } 796 797 } 798 799 return nil 800 }) 801 } 802 803 func TestInitConsistentState(t *testing.T) { 804 // Boilerplate for creating a chain. 805 dbName := "TestFlushOnPrune" 806 chain, tearDown, err := chainSetup(dbName, &chaincfg.MainNetParams) 807 if err != nil { 808 panic(fmt.Sprintf("error loading blockchain with database: %v", err)) 809 } 810 defer tearDown() 811 chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 812 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage 813 814 // Read blocks from the file. 815 blocks, err := loadBlocks("blk_0_to_14131.dat") 816 if err != nil { 817 t.Fatalf("failed to read block from file. %v", err) 818 } 819 820 // Sync up to height 13,000. Flush the utxocache at height 11_000. 821 cacheFlushHeight := 9000 822 initialSyncHeight := 12_000 823 for i, block := range blocks { 824 if i == 0 { 825 // Skip the genesis block. 826 continue 827 } 828 829 isMainChain, _, err := chain.ProcessBlock(block, BFNone) 830 if err != nil { 831 t.Fatal(err) 832 } 833 834 if !isMainChain { 835 t.Fatalf("expected block %s to be on the main chain", block.Hash()) 836 } 837 838 if i == cacheFlushHeight { 839 err = chain.FlushUtxoCache(FlushRequired) 840 if err != nil { 841 t.Fatal(err) 842 } 843 } 844 if i == initialSyncHeight { 845 break 846 } 847 } 848 849 // Sanity check. 850 if chain.BestSnapshot().Height != int32(initialSyncHeight) { 851 t.Fatalf("expected the chain to sync up to height %d", initialSyncHeight) 852 } 853 854 // Close the database without flushing the utxocache. This leaves the 855 // chaintip at height 13,000 but the utxocache consistent state at 11,000. 856 err = chain.db.Close() 857 if err != nil { 858 t.Fatal(err) 859 } 860 chain.db = nil 861 862 // Re-open the database and pass the re-opened db to internal structs. 863 dbPath := filepath.Join(testDbRoot, dbName) 864 ndb, err := database.Open(testDbType, dbPath, blockDataNet) 865 if err != nil { 866 t.Fatal(err) 867 } 868 chain.db = ndb 869 chain.utxoCache.db = ndb 870 chain.index.db = ndb 871 872 // Sanity check to see that the utxo cache was flushed before the 873 // current chain tip. 874 var statusBytes []byte 875 ndb.View(func(dbTx database.Tx) error { 876 statusBytes = dbFetchUtxoStateConsistency(dbTx) 877 return nil 878 }) 879 statusHash, err := chainhash.NewHash(statusBytes) 880 if err != nil { 881 t.Fatal(err) 882 } 883 if !statusHash.IsEqual(blocks[cacheFlushHeight].Hash()) { 884 t.Fatalf("expected the utxocache to be flushed at "+ 885 "block hash %s but got %s", 886 blocks[cacheFlushHeight].Hash(), statusHash) 887 } 888 889 // Call InitConsistentState. This will make the utxocache catch back 890 // up to the tip. 891 err = chain.InitConsistentState(chain.bestChain.tip(), nil) 892 if err != nil { 893 t.Fatal(err) 894 } 895 896 // Sync the reset of the blocks. 897 for i, block := range blocks { 898 if i <= initialSyncHeight { 899 continue 900 } 901 isMainChain, _, err := chain.ProcessBlock(block, BFNone) 902 if err != nil { 903 t.Fatal(err) 904 } 905 906 if !isMainChain { 907 t.Fatalf("expected block %s to be on the main chain", block.Hash()) 908 } 909 } 910 911 if chain.BestSnapshot().Height != blocks[len(blocks)-1].Height() { 912 t.Fatalf("expected the chain to sync up to height %d", 913 blocks[len(blocks)-1].Height()) 914 } 915 }