// Copyright (c) 2023 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package blockchain import ( "crypto/sha256" "encoding/binary" "fmt" "path/filepath" "reflect" "sync" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/database/ffldb" "github.com/btcsuite/btcd/wire" ) func TestMapSlice(t *testing.T) { tests := []struct { keys []wire.OutPoint }{ { keys: func() []wire.OutPoint { outPoints := make([]wire.OutPoint, 1000) for i := uint32(0); i < uint32(len(outPoints)); i++ { var buf [4]byte binary.BigEndian.PutUint32(buf[:], i) hash := sha256.Sum256(buf[:]) op := wire.OutPoint{Hash: hash, Index: i} outPoints[i] = op } return outPoints }(), }, } for _, test := range tests { m := make(map[wire.OutPoint]*UtxoEntry) maxSize := calculateRoughMapSize(1000, bucketSize) maxEntriesFirstMap := 500 ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap) ms := mapSlice{ maps: []map[wire.OutPoint]*UtxoEntry{ms1}, maxEntries: []int{maxEntriesFirstMap}, maxTotalMemoryUsage: uint64(maxSize), } for _, key := range test.keys { m[key] = nil ms.put(key, nil, 0) } // Put in the same elements twice to test that the map slice won't hold duplicates. for _, key := range test.keys { m[key] = nil ms.put(key, nil, 0) } if len(m) != ms.length() { t.Fatalf("expected len of %d, got %d", len(m), ms.length()) } // Delete the first element in the first map. ms.delete(test.keys[0]) delete(m, test.keys[0]) // Try to insert the last element in the mapslice again. ms.put(test.keys[len(test.keys)-1], &UtxoEntry{}, 0) m[test.keys[len(test.keys)-1]] = &UtxoEntry{} // Check that the duplicate didn't make it in. if len(m) != ms.length() { t.Fatalf("expected len of %d, got %d", len(m), ms.length()) } ms.put(test.keys[0], &UtxoEntry{}, 0) m[test.keys[0]] = &UtxoEntry{} if len(m) != ms.length() { t.Fatalf("expected len of %d, got %d", len(m), ms.length()) } for _, key := range test.keys { expected, found := m[key] if !found { t.Fatalf("expected key %s to exist in the go map", key.String()) } got, found := ms.get(key) if !found { t.Fatalf("expected key %s to exist in the map slice", key.String()) } if !reflect.DeepEqual(got, expected) { t.Fatalf("expected value of %v, got %v", expected, got) } } } } // TestMapsliceConcurrency just tests that the mapslice won't result in a panic // on concurrent access. func TestMapsliceConcurrency(t *testing.T) { tests := []struct { keys []wire.OutPoint }{ { keys: func() []wire.OutPoint { outPoints := make([]wire.OutPoint, 10000) for i := uint32(0); i < uint32(len(outPoints)); i++ { var buf [4]byte binary.BigEndian.PutUint32(buf[:], i) hash := sha256.Sum256(buf[:]) op := wire.OutPoint{Hash: hash, Index: i} outPoints[i] = op } return outPoints }(), }, } for _, test := range tests { maxSize := calculateRoughMapSize(1000, bucketSize) maxEntriesFirstMap := 500 ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap) ms := mapSlice{ maps: []map[wire.OutPoint]*UtxoEntry{ms1}, maxEntries: []int{maxEntriesFirstMap}, maxTotalMemoryUsage: uint64(maxSize), } var wg sync.WaitGroup wg.Add(1) go func(m *mapSlice, keys []wire.OutPoint) { defer wg.Done() for i := 0; i < 5000; i++ { m.put(keys[i], nil, 0) } }(&ms, test.keys) wg.Add(1) go func(m *mapSlice, keys []wire.OutPoint) { defer wg.Done() for i := 5000; i < 10000; i++ { m.put(keys[i], nil, 0) } }(&ms, test.keys) wg.Add(1) go func(m *mapSlice) { defer wg.Done() for i := 0; i < 10000; i++ { m.size() } }(&ms) wg.Add(1) go func(m *mapSlice) { defer wg.Done() for i := 0; i < 10000; i++ { m.length() } }(&ms) wg.Add(1) go func(m *mapSlice, keys []wire.OutPoint) { defer wg.Done() for i := 0; i < 10000; i++ { m.get(keys[i]) } }(&ms, test.keys) wg.Add(1) go func(m *mapSlice, keys []wire.OutPoint) { defer wg.Done() for i := 0; i < 5000; i++ { m.delete(keys[i]) } }(&ms, test.keys) wg.Wait() } } // getValidP2PKHScript returns a valid P2PKH script. Useful as unspendables cannot be // added to the cache. func getValidP2PKHScript() []byte { validP2PKHScript := []byte{ // OP_DUP 0x76, // OP_HASH160 0xa9, // OP_DATA_20 0x14, // <20-byte pubkey hash> 0xf0, 0x7a, 0xb8, 0xce, 0x72, 0xda, 0x4e, 0x76, 0x0b, 0x74, 0x7d, 0x48, 0xd6, 0x65, 0xec, 0x96, 0xad, 0xf0, 0x24, 0xf5, // OP_EQUALVERIFY 0x88, // OP_CHECKSIG 0xac, } return validP2PKHScript } // outpointFromInt generates an outpoint from an int by hashing the int and making // the given int the index. func outpointFromInt(i int) wire.OutPoint { // Boilerplate to create an outpoint. var buf [4]byte binary.BigEndian.PutUint32(buf[:], uint32(i)) hash := sha256.Sum256(buf[:]) return wire.OutPoint{Hash: hash, Index: uint32(i)} } func TestUtxoCacheEntrySize(t *testing.T) { type block struct { txOuts []*wire.TxOut outOps []wire.OutPoint txIns []*wire.TxIn } tests := []struct { name string blocks []block expectedSize uint64 }{ { name: "one entry", blocks: func() []block { return []block{ { txOuts: []*wire.TxOut{ {Value: 10000, PkScript: getValidP2PKHScript()}, }, outOps: []wire.OutPoint{ outpointFromInt(0), }, }, } }(), expectedSize: pubKeyHashLen + baseEntrySize, }, { name: "10 entries, 4 spend", blocks: func() []block { blocks := make([]block, 0, 10) for i := 0; i < 10; i++ { op := outpointFromInt(i) block := block{ txOuts: []*wire.TxOut{ {Value: 10000, PkScript: getValidP2PKHScript()}, }, outOps: []wire.OutPoint{ op, }, } // Spend all outs in blocks less than 4. if i < 4 { block.txIns = []*wire.TxIn{ {PreviousOutPoint: op}, } } blocks = append(blocks, block) } return blocks }(), // Multiplied by 6 since we'll have 6 entries left. expectedSize: (pubKeyHashLen + baseEntrySize) * 6, }, { name: "spend everything", blocks: func() []block { blocks := make([]block, 0, 500) for i := 0; i < 500; i++ { op := outpointFromInt(i) block := block{ txOuts: []*wire.TxOut{ {Value: 1000, PkScript: getValidP2PKHScript()}, }, outOps: []wire.OutPoint{ op, }, } // Spend all outs in blocks less than 4. block.txIns = []*wire.TxIn{ {PreviousOutPoint: op}, } blocks = append(blocks, block) } return blocks }(), expectedSize: 0, }, } for _, test := range tests { // Size is just something big enough so that the mapslice doesn't // run out of memory. s := newUtxoCache(nil, 1*1024*1024) for height, block := range test.blocks { for i, out := range block.txOuts { s.addTxOut(block.outOps[i], out, true, int32(height)) } for _, in := range block.txIns { s.addTxIn(in, nil) } } if s.totalEntryMemory != test.expectedSize { t.Errorf("Failed test %s. Expected size of %d, got %d", test.name, test.expectedSize, s.totalEntryMemory) } } } // assertConsistencyState asserts the utxo consistency states of the blockchain. func assertConsistencyState(chain *BlockChain, hash *chainhash.Hash) error { var bytes []byte err := chain.db.View(func(dbTx database.Tx) (err error) { bytes = dbFetchUtxoStateConsistency(dbTx) return }) if err != nil { return fmt.Errorf("Error fetching utxo state consistency: %v", err) } actualHash, err := chainhash.NewHash(bytes) if err != nil { return err } if !actualHash.IsEqual(hash) { return fmt.Errorf("Unexpected consistency hash: %v instead of %v", actualHash, hash) } return nil } // assertNbEntriesOnDisk asserts that the total number of utxo entries on the // disk is equal to the given expected number. func assertNbEntriesOnDisk(chain *BlockChain, expectedNumber int) error { var nb int err := chain.db.View(func(dbTx database.Tx) error { cursor := dbTx.Metadata().Bucket(utxoSetBucketName).Cursor() nb = 0 for b := cursor.First(); b; b = cursor.Next() { nb++ _, err := deserializeUtxoEntry(cursor.Value()) if err != nil { return fmt.Errorf("Failed to deserialize entry: %v", err) } } return nil }) if err != nil { return fmt.Errorf("Error fetching utxo entries: %v", err) } if nb != expectedNumber { return fmt.Errorf("Expected %d elements in the UTXO set, but found %d", expectedNumber, nb) } return nil } // utxoCacheTestChain creates a test BlockChain to be used for utxo cache tests. // It uses the regression test parameters, a coin matutiry of 1 block and sets // the cache size limit to 10 MiB. func utxoCacheTestChain(testName string) (*BlockChain, *chaincfg.Params, func()) { params := chaincfg.RegressionNetParams chain, tearDown, err := chainSetup(testName, ¶ms) if err != nil { panic(fmt.Sprintf("error loading blockchain with database: %v", err)) } chain.TstSetCoinbaseMaturity(1) chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage return chain, ¶ms, tearDown } func TestUtxoCacheFlush(t *testing.T) { chain, params, tearDown := utxoCacheTestChain("TestUtxoCacheFlush") defer tearDown() cache := chain.utxoCache tip := btcutil.NewBlock(params.GenesisBlock) // The chainSetup init triggers the consistency status write. err := assertConsistencyState(chain, params.GenesisHash) if err != nil { t.Fatal(err) } err = assertNbEntriesOnDisk(chain, 0) if err != nil { t.Fatal(err) } // LastFlushHash starts with genesis. if cache.lastFlushHash != *params.GenesisHash { t.Fatalf("lastFlushHash before first flush expected to be "+ "genesis block hash, instead was %v", cache.lastFlushHash) } // First, add 10 utxos without flushing. outPoints := make([]wire.OutPoint, 10) for i := range outPoints { op := outpointFromInt(i) outPoints[i] = op // Add the txout. txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()} cache.addTxOut(op, &txOut, true, int32(i)) } if cache.cachedEntries.length() != len(outPoints) { t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length()) } // All entries should be fresh and modified. for _, m := range cache.cachedEntries.maps { for outpoint, entry := range m { if entry == nil { t.Fatalf("Unexpected nil entry found for %v", outpoint) } if !entry.isModified() { t.Fatal("Entry should be marked modified") } if !entry.isFresh() { t.Fatal("Entry should be marked fresh") } } } // Spend the last outpoint and pop it off from the outpoints slice. var spendOp wire.OutPoint spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1] cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil) if cache.cachedEntries.length() != len(outPoints) { t.Fatalf("Expected %d entries, has %d instead", len(outPoints), cache.cachedEntries.length()) } // Not flushed yet. err = assertConsistencyState(chain, params.GenesisHash) if err != nil { t.Fatal(err) } err = assertNbEntriesOnDisk(chain, 0) if err != nil { t.Fatal(err) } // Flush. err = chain.db.Update(func(dbTx database.Tx) error { return cache.flush(dbTx, FlushRequired, chain.stateSnapshot) }) if err != nil { t.Fatalf("unexpected error while flushing cache: %v", err) } if cache.cachedEntries.length() != 0 { t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) } err = assertConsistencyState(chain, tip.Hash()) if err != nil { t.Fatal(err) } err = assertNbEntriesOnDisk(chain, len(outPoints)) if err != nil { t.Fatal(err) } // Fetch the flushed utxos. entries, err := cache.fetchEntries(outPoints) if err != nil { t.Fatal(err) } // Check that the returned entries are not marked fresh and modified. for _, entry := range entries { if entry.isFresh() { t.Fatal("Entry should not be marked fresh") } if entry.isModified() { t.Fatal("Entry should not be marked modified") } } // Check that the fetched entries in the cache are not marked fresh and modified. for _, m := range cache.cachedEntries.maps { for outpoint, elem := range m { if elem == nil { t.Fatalf("Unexpected nil entry found for %v", outpoint) } if elem.isFresh() { t.Fatal("Entry should not be marked fresh") } if elem.isModified() { t.Fatal("Entry should not be marked modified") } } } // Spend 5 utxos. prevLen := len(outPoints) for i := 0; i < 5; i++ { spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1] cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil) } // Should still have the entries in cache so they can be flushed to disk. if cache.cachedEntries.length() != prevLen { t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length()) } // Flush. err = chain.db.Update(func(dbTx database.Tx) error { return cache.flush(dbTx, FlushRequired, chain.stateSnapshot) }) if err != nil { t.Fatalf("unexpected error while flushing cache: %v", err) } if cache.cachedEntries.length() != 0 { t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) } err = assertConsistencyState(chain, tip.Hash()) if err != nil { t.Fatal(err) } err = assertNbEntriesOnDisk(chain, len(outPoints)) if err != nil { t.Fatal(err) } // Add 5 utxos without flushing and test for periodic flushes. outPoints1 := make([]wire.OutPoint, 5) for i := range outPoints1 { // i + prevLen here to avoid collision since we're just hashing // the int. op := outpointFromInt(i + prevLen) outPoints1[i] = op // Add the txout. txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()} cache.addTxOut(op, &txOut, true, int32(i+prevLen)) } if cache.cachedEntries.length() != len(outPoints1) { t.Fatalf("Expected %d entries, has %d instead", len(outPoints1), cache.cachedEntries.length()) } // Attempt to flush with flush periodic. Shouldn't flush. err = chain.db.Update(func(dbTx database.Tx) error { return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot) }) if err != nil { t.Fatalf("unexpected error while flushing cache: %v", err) } if cache.cachedEntries.length() == 0 { t.Fatalf("Expected %d entries, has %d instead", len(outPoints1), cache.cachedEntries.length()) } // Arbitrarily set the last flush time to 6 minutes ago. cache.lastFlushTime = time.Now().Add(-time.Minute * 6) // Attempt to flush with flush periodic. Should flush now. err = chain.db.Update(func(dbTx database.Tx) error { return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot) }) if err != nil { t.Fatalf("unexpected error while flushing cache: %v", err) } if cache.cachedEntries.length() != 0 { t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length()) } err = assertConsistencyState(chain, tip.Hash()) if err != nil { t.Fatal(err) } err = assertNbEntriesOnDisk(chain, len(outPoints)+len(outPoints1)) if err != nil { t.Fatal(err) } } func TestFlushNeededAfterPrune(t *testing.T) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) chain.utxoCache = newUtxoCache(nil, 0) branchNodes := chainedNodes(chain.bestChain.Genesis(), 18) for _, node := range branchNodes { chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branchNodes)) tests := []struct { name string lastFlushHash chainhash.Hash delHashes []chainhash.Hash expected bool }{ { name: "deleted block up to height 9, last flush hash at block 10", delHashes: func() []chainhash.Hash { delBlockHashes := make([]chainhash.Hash, 0, 9) for i := range branchNodes { if branchNodes[i].height < 10 { delBlockHashes = append(delBlockHashes, branchNodes[i].hash) } } return delBlockHashes }(), lastFlushHash: func() chainhash.Hash { // Just some sanity checking to make sure the height is 10. if branchNodes[9].height != 10 { panic("was looking for height 10") } return branchNodes[9].hash }(), expected: false, }, { name: "deleted blocks up to height 10, last flush hash at block 10", delHashes: func() []chainhash.Hash { delBlockHashes := make([]chainhash.Hash, 0, 10) for i := range branchNodes { if branchNodes[i].height < 11 { delBlockHashes = append(delBlockHashes, branchNodes[i].hash) } } return delBlockHashes }(), lastFlushHash: func() chainhash.Hash { // Just some sanity checking to make sure the height is 10. if branchNodes[9].height != 10 { panic("was looking for height 10") } return branchNodes[9].hash }(), expected: true, }, { name: "deleted block height 17, last flush hash at block 5", delHashes: func() []chainhash.Hash { delBlockHashes := make([]chainhash.Hash, 1) delBlockHashes[0] = branchNodes[16].hash // Just some sanity checking to make sure the height is 10. if branchNodes[16].height != 17 { panic("was looking for height 17") } return delBlockHashes }(), lastFlushHash: func() chainhash.Hash { // Just some sanity checking to make sure the height is 10. if branchNodes[4].height != 5 { panic("was looking for height 5") } return branchNodes[4].hash }(), expected: true, }, { name: "deleted block height 3, last flush hash at block 4", delHashes: func() []chainhash.Hash { delBlockHashes := make([]chainhash.Hash, 1) delBlockHashes[0] = branchNodes[2].hash // Just some sanity checking to make sure the height is 10. if branchNodes[2].height != 3 { panic("was looking for height 3") } return delBlockHashes }(), lastFlushHash: func() chainhash.Hash { // Just some sanity checking to make sure the height is 10. if branchNodes[3].height != 4 { panic("was looking for height 4") } return branchNodes[3].hash }(), expected: false, }, } for _, test := range tests { chain.utxoCache.lastFlushHash = test.lastFlushHash got, err := chain.flushNeededAfterPrune(test.delHashes) if err != nil { t.Fatal(err) } if got != test.expected { t.Fatalf("for test %s, expected need flush to return %v but got %v", test.name, test.expected, got) } } } func TestFlushOnPrune(t *testing.T) { chain, tearDown, err := chainSetup("TestFlushOnPrune", &chaincfg.MainNetParams) if err != nil { panic(fmt.Sprintf("error loading blockchain with database: %v", err)) } defer tearDown() chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage // Set the maxBlockFileSize and the prune target small so that we can trigger a // prune to happen. maxBlockFileSize := uint32(8192) chain.pruneTarget = uint64(maxBlockFileSize) * 2 // Read blocks from the file. blocks, err := loadBlocks("blk_0_to_14131.dat") if err != nil { t.Fatalf("failed to read block from file. %v", err) } syncBlocks := func() { // Modify block 1 to be a different hash. This is to artificially // create a stale branch in the chain. staleMsgBlock := blocks[1].MsgBlock().Copy() staleMsgBlock.Header.Nonce = 0 staleBlock := btcutil.NewBlock(staleMsgBlock) // Add the stale block here to create a chain view like so. The // block will be the main chain at first but become stale as we // keep adding blocks. BFNoPoWCheck is given as the pow check will // fail. // // (genesis block) -> 1 -> 2 -> 3 -> ... // \-> 1a _, _, err = chain.ProcessBlock(staleBlock, BFNoPoWCheck) if err != nil { t.Fatal(err) } for i, block := range blocks { if i == 0 { // Skip the genesis block. continue } _, _, err = chain.ProcessBlock(block, BFNone) if err != nil { t.Fatalf("Failed to process block %v(%v). %v", block.Hash().String(), block.Height(), err) } } } // Sync the chain. ffldb.TstRunWithMaxBlockFileSize(chain.db, maxBlockFileSize, syncBlocks) // Function that errors out if the block that should exist doesn't exist. shouldExist := func(dbTx database.Tx, blockHash *chainhash.Hash) error { bytes, err := dbTx.FetchBlock(blockHash) if err != nil { return err } block, err := btcutil.NewBlockFromBytes(bytes) if err != nil { return fmt.Errorf("didn't find block %v. %v", blockHash, err) } if !block.Hash().IsEqual(blockHash) { return fmt.Errorf("expected to find block %v but got %v", blockHash, block.Hash()) } return nil } // Function that errors out if the block that shouldn't exist exists. shouldNotExist := func(dbTx database.Tx, blockHash *chainhash.Hash) error { bytes, err := dbTx.FetchBlock(chaincfg.MainNetParams.GenesisHash) if err == nil { return fmt.Errorf("expected block %s to be pruned", blockHash.String()) } if len(bytes) != 0 { return fmt.Errorf("expected block %s to be pruned but got %v", blockHash, bytes) } return nil } // The below code checks that the correct blocks were pruned. err = chain.db.View(func(dbTx database.Tx) error { exist := false for _, block := range blocks { // Blocks up to the last flush hash should not exist. // The utxocache is big enough so that it shouldn't flush // on it being full. It should only flush on prunes. if block.Hash().IsEqual(&chain.utxoCache.lastFlushHash) { exist = true } if exist { err = shouldExist(dbTx, block.Hash()) if err != nil { return err } } else { err = shouldNotExist(dbTx, block.Hash()) if err != nil { return err } } } return nil }) if err != nil { t.Fatal(err) } } func TestInitConsistentState(t *testing.T) { // Boilerplate for creating a chain. dbName := "TestFlushOnPrune" chain, tearDown, err := chainSetup(dbName, &chaincfg.MainNetParams) if err != nil { panic(fmt.Sprintf("error loading blockchain with database: %v", err)) } defer tearDown() chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024 chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage // Read blocks from the file. blocks, err := loadBlocks("blk_0_to_14131.dat") if err != nil { t.Fatalf("failed to read block from file. %v", err) } // Sync up to height 13,000. Flush the utxocache at height 11_000. cacheFlushHeight := 9000 initialSyncHeight := 12_000 for i, block := range blocks { if i == 0 { // Skip the genesis block. continue } isMainChain, _, err := chain.ProcessBlock(block, BFNone) if err != nil { t.Fatal(err) } if !isMainChain { t.Fatalf("expected block %s to be on the main chain", block.Hash()) } if i == cacheFlushHeight { err = chain.FlushUtxoCache(FlushRequired) if err != nil { t.Fatal(err) } } if i == initialSyncHeight { break } } // Sanity check. if chain.BestSnapshot().Height != int32(initialSyncHeight) { t.Fatalf("expected the chain to sync up to height %d", initialSyncHeight) } // Close the database without flushing the utxocache. This leaves the // chaintip at height 13,000 but the utxocache consistent state at 11,000. err = chain.db.Close() if err != nil { t.Fatal(err) } chain.db = nil // Re-open the database and pass the re-opened db to internal structs. dbPath := filepath.Join(testDbRoot, dbName) ndb, err := database.Open(testDbType, dbPath, blockDataNet) if err != nil { t.Fatal(err) } chain.db = ndb chain.utxoCache.db = ndb chain.index.db = ndb // Sanity check to see that the utxo cache was flushed before the // current chain tip. var statusBytes []byte ndb.View(func(dbTx database.Tx) error { statusBytes = dbFetchUtxoStateConsistency(dbTx) return nil }) statusHash, err := chainhash.NewHash(statusBytes) if err != nil { t.Fatal(err) } if !statusHash.IsEqual(blocks[cacheFlushHeight].Hash()) { t.Fatalf("expected the utxocache to be flushed at "+ "block hash %s but got %s", blocks[cacheFlushHeight].Hash(), statusHash) } // Call InitConsistentState. This will make the utxocache catch back // up to the tip. err = chain.InitConsistentState(chain.bestChain.tip(), nil) if err != nil { t.Fatal(err) } // Sync the reset of the blocks. for i, block := range blocks { if i <= initialSyncHeight { continue } isMainChain, _, err := chain.ProcessBlock(block, BFNone) if err != nil { t.Fatal(err) } if !isMainChain { t.Fatalf("expected block %s to be on the main chain", block.Hash()) } } if chain.BestSnapshot().Height != blocks[len(blocks)-1].Height() { t.Fatalf("expected the chain to sync up to height %d", blocks[len(blocks)-1].Height()) } }