blockmanager_tests.cpp
1 // Copyright (c) 2022 The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <chainparams.h> 6 #include <clientversion.h> 7 #include <node/blockstorage.h> 8 #include <node/context.h> 9 #include <node/kernel_notifications.h> 10 #include <script/solver.h> 11 #include <primitives/block.h> 12 #include <util/chaintype.h> 13 #include <validation.h> 14 15 #include <boost/test/unit_test.hpp> 16 #include <test/util/logging.h> 17 #include <test/util/setup_common.h> 18 19 using node::BLOCK_SERIALIZATION_HEADER_SIZE; 20 using node::BlockManager; 21 using node::KernelNotifications; 22 using node::MAX_BLOCKFILE_SIZE; 23 24 // use BasicTestingSetup here for the data directory configuration, setup, and cleanup 25 BOOST_FIXTURE_TEST_SUITE(blockmanager_tests, BasicTestingSetup) 26 27 BOOST_AUTO_TEST_CASE(blockmanager_find_block_pos) 28 { 29 const auto params {CreateChainParams(ArgsManager{}, ChainType::MAIN)}; 30 KernelNotifications notifications{*Assert(m_node.shutdown), m_node.exit_status}; 31 const BlockManager::Options blockman_opts{ 32 .chainparams = *params, 33 .blocks_dir = m_args.GetBlocksDirPath(), 34 .notifications = notifications, 35 }; 36 BlockManager blockman{*Assert(m_node.shutdown), blockman_opts}; 37 // simulate adding a genesis block normally 38 BOOST_CHECK_EQUAL(blockman.SaveBlockToDisk(params->GenesisBlock(), 0, nullptr).nPos, BLOCK_SERIALIZATION_HEADER_SIZE); 39 // simulate what happens during reindex 40 // simulate a well-formed genesis block being found at offset 8 in the blk00000.dat file 41 // the block is found at offset 8 because there is an 8 byte serialization header 42 // consisting of 4 magic bytes + 4 length bytes before each block in a well-formed blk file. 43 FlatFilePos pos{0, BLOCK_SERIALIZATION_HEADER_SIZE}; 44 BOOST_CHECK_EQUAL(blockman.SaveBlockToDisk(params->GenesisBlock(), 0, &pos).nPos, BLOCK_SERIALIZATION_HEADER_SIZE); 45 // now simulate what happens after reindex for the first new block processed 46 // the actual block contents don't matter, just that it's a block. 47 // verify that the write position is at offset 0x12d. 48 // this is a check to make sure that https://github.com/bitcoin/bitcoin/issues/21379 does not recur 49 // 8 bytes (for serialization header) + 285 (for serialized genesis block) = 293 50 // add another 8 bytes for the second block's serialization header and we get 293 + 8 = 301 51 FlatFilePos actual{blockman.SaveBlockToDisk(params->GenesisBlock(), 1, nullptr)}; 52 BOOST_CHECK_EQUAL(actual.nPos, BLOCK_SERIALIZATION_HEADER_SIZE + ::GetSerializeSize(TX_WITH_WITNESS(params->GenesisBlock())) + BLOCK_SERIALIZATION_HEADER_SIZE); 53 } 54 55 BOOST_FIXTURE_TEST_CASE(blockmanager_scan_unlink_already_pruned_files, TestChain100Setup) 56 { 57 // Cap last block file size, and mine new block in a new block file. 58 const auto& chainman = Assert(m_node.chainman); 59 auto& blockman = chainman->m_blockman; 60 const CBlockIndex* old_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())}; 61 WITH_LOCK(chainman->GetMutex(), blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE); 62 CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); 63 64 // Prune the older block file, but don't unlink it 65 int file_number; 66 { 67 LOCK(chainman->GetMutex()); 68 file_number = old_tip->GetBlockPos().nFile; 69 blockman.PruneOneBlockFile(file_number); 70 } 71 72 const FlatFilePos pos(file_number, 0); 73 74 // Check that the file is not unlinked after ScanAndUnlinkAlreadyPrunedFiles 75 // if m_have_pruned is not yet set 76 WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles()); 77 BOOST_CHECK(!blockman.OpenBlockFile(pos, true).IsNull()); 78 79 // Check that the file is unlinked after ScanAndUnlinkAlreadyPrunedFiles 80 // once m_have_pruned is set 81 blockman.m_have_pruned = true; 82 WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles()); 83 BOOST_CHECK(blockman.OpenBlockFile(pos, true).IsNull()); 84 85 // Check that calling with already pruned files doesn't cause an error 86 WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles()); 87 88 // Check that the new tip file has not been removed 89 const CBlockIndex* new_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())}; 90 BOOST_CHECK_NE(old_tip, new_tip); 91 const int new_file_number{WITH_LOCK(chainman->GetMutex(), return new_tip->GetBlockPos().nFile)}; 92 const FlatFilePos new_pos(new_file_number, 0); 93 BOOST_CHECK(!blockman.OpenBlockFile(new_pos, true).IsNull()); 94 } 95 96 BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup) 97 { 98 // The goal of the function is to return the first not pruned block in the range [upper_block, lower_block]. 99 LOCK(::cs_main); 100 auto& chainman = m_node.chainman; 101 auto& blockman = chainman->m_blockman; 102 const CBlockIndex& tip = *chainman->ActiveTip(); 103 104 // Function to prune all blocks from 'last_pruned_block' down to the genesis block 105 const auto& func_prune_blocks = [&](CBlockIndex* last_pruned_block) 106 { 107 LOCK(::cs_main); 108 CBlockIndex* it = last_pruned_block; 109 while (it != nullptr && it->nStatus & BLOCK_HAVE_DATA) { 110 it->nStatus &= ~BLOCK_HAVE_DATA; 111 it = it->pprev; 112 } 113 }; 114 115 // 1) Return genesis block when all blocks are available 116 BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), chainman->ActiveChain()[0]); 117 BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *chainman->ActiveChain()[0])); 118 119 // 2) Check lower_block when all blocks are available 120 CBlockIndex* lower_block = chainman->ActiveChain()[tip.nHeight / 2]; 121 BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *lower_block)); 122 123 // Prune half of the blocks 124 int height_to_prune = tip.nHeight / 2; 125 CBlockIndex* first_available_block = chainman->ActiveChain()[height_to_prune + 1]; 126 CBlockIndex* last_pruned_block = first_available_block->pprev; 127 func_prune_blocks(last_pruned_block); 128 129 // 3) The last block not pruned is in-between upper-block and the genesis block 130 BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), first_available_block); 131 BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *first_available_block)); 132 BOOST_CHECK(!blockman.CheckBlockDataAvailability(tip, *last_pruned_block)); 133 } 134 135 BOOST_AUTO_TEST_CASE(blockmanager_flush_block_file) 136 { 137 KernelNotifications notifications{*Assert(m_node.shutdown), m_node.exit_status}; 138 node::BlockManager::Options blockman_opts{ 139 .chainparams = Params(), 140 .blocks_dir = m_args.GetBlocksDirPath(), 141 .notifications = notifications, 142 }; 143 BlockManager blockman{*Assert(m_node.shutdown), blockman_opts}; 144 145 // Test blocks with no transactions, not even a coinbase 146 CBlock block1; 147 block1.nVersion = 1; 148 CBlock block2; 149 block2.nVersion = 2; 150 CBlock block3; 151 block3.nVersion = 3; 152 153 // They are 80 bytes header + 1 byte 0x00 for vtx length 154 constexpr int TEST_BLOCK_SIZE{81}; 155 156 // Blockstore is empty 157 BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), 0); 158 159 // Write the first block; dbp=nullptr means this block doesn't already have a disk 160 // location, so allocate a free location and write it there. 161 FlatFilePos pos1{blockman.SaveBlockToDisk(block1, /*nHeight=*/1, /*dbp=*/nullptr)}; 162 163 // Write second block 164 FlatFilePos pos2{blockman.SaveBlockToDisk(block2, /*nHeight=*/2, /*dbp=*/nullptr)}; 165 166 // Two blocks in the file 167 BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), (TEST_BLOCK_SIZE + BLOCK_SERIALIZATION_HEADER_SIZE) * 2); 168 169 // First two blocks are written as expected 170 // Errors are expected because block data is junk, thrown AFTER successful read 171 CBlock read_block; 172 BOOST_CHECK_EQUAL(read_block.nVersion, 0); 173 { 174 ASSERT_DEBUG_LOG("ReadBlockFromDisk: Errors in block header"); 175 BOOST_CHECK(!blockman.ReadBlockFromDisk(read_block, pos1)); 176 BOOST_CHECK_EQUAL(read_block.nVersion, 1); 177 } 178 { 179 ASSERT_DEBUG_LOG("ReadBlockFromDisk: Errors in block header"); 180 BOOST_CHECK(!blockman.ReadBlockFromDisk(read_block, pos2)); 181 BOOST_CHECK_EQUAL(read_block.nVersion, 2); 182 } 183 184 // When FlatFilePos* dbp is given, SaveBlockToDisk() will not write or 185 // overwrite anything to the flat file block storage. It will, however, 186 // update the blockfile metadata. This is to facilitate reindexing 187 // when the user has the blocks on disk but the metadata is being rebuilt. 188 // Verify this behavior by attempting (and failing) to write block 3 data 189 // to block 2 location. 190 CBlockFileInfo* block_data = blockman.GetBlockFileInfo(0); 191 BOOST_CHECK_EQUAL(block_data->nBlocks, 2); 192 BOOST_CHECK(blockman.SaveBlockToDisk(block3, /*nHeight=*/3, /*dbp=*/&pos2) == pos2); 193 // Metadata is updated... 194 BOOST_CHECK_EQUAL(block_data->nBlocks, 3); 195 // ...but there are still only two blocks in the file 196 BOOST_CHECK_EQUAL(blockman.CalculateCurrentUsage(), (TEST_BLOCK_SIZE + BLOCK_SERIALIZATION_HEADER_SIZE) * 2); 197 198 // Block 2 was not overwritten: 199 // SaveBlockToDisk() did not call WriteBlockToDisk() because `FlatFilePos* dbp` was non-null 200 blockman.ReadBlockFromDisk(read_block, pos2); 201 BOOST_CHECK_EQUAL(read_block.nVersion, 2); 202 } 203 204 BOOST_AUTO_TEST_SUITE_END()