|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2021 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | +"""Test for assumeutxo, a means of quickly bootstrapping a node using |
| 6 | +a serialized version of the UTXO set at a certain height, which corresponds |
| 7 | +to a hash that has been compiled into bitcoind. |
| 8 | +
|
| 9 | +The assumeutxo value generated and used here is committed to in |
| 10 | +`CRegTestParams::m_assumeutxo_data` in `src/chainparams.cpp`. |
| 11 | +
|
| 12 | +## Possible test improvements |
| 13 | +
|
| 14 | +- TODO: test submitting a transaction and verifying it appears in mempool |
| 15 | +- TODO: test what happens with -reindex and -reindex-chainstate before the |
| 16 | + snapshot is validated, and make sure it's deleted successfully. |
| 17 | +
|
| 18 | +Interesting test cases could be loading an assumeutxo snapshot file with: |
| 19 | +
|
| 20 | +- TODO: An invalid hash |
| 21 | +- TODO: Valid hash but invalid snapshot file (bad coin height or truncated file or |
| 22 | + bad other serialization) |
| 23 | +- TODO: Valid snapshot file, but referencing an unknown block |
| 24 | +- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be |
| 25 | + invalid, or has an invalid parent |
| 26 | +- TODO: Valid snapshot file and snapshot block, but the block is not on the |
| 27 | + most-work chain |
| 28 | +
|
| 29 | +Interesting starting states could be loading a snapshot when the current chain tip is: |
| 30 | +
|
| 31 | +- TODO: An ancestor of snapshot block |
| 32 | +- TODO: Not an ancestor of the snapshot block but has less work |
| 33 | +- TODO: The snapshot block |
| 34 | +- TODO: A descendant of the snapshot block |
| 35 | +- TODO: Not an ancestor or a descendant of the snapshot block and has more work |
| 36 | +
|
| 37 | +""" |
| 38 | +from test_framework.test_framework import BitcoinTestFramework |
| 39 | +from test_framework.util import assert_equal, wait_until_helper |
| 40 | + |
| 41 | +START_HEIGHT = 199 |
| 42 | +SNAPSHOT_BASE_HEIGHT = 299 |
| 43 | +FINAL_HEIGHT = 399 |
| 44 | +COMPLETE_IDX = {'synced': True, 'best_block_height': FINAL_HEIGHT} |
| 45 | + |
| 46 | + |
| 47 | +class AssumeutxoTest(BitcoinTestFramework): |
| 48 | + |
| 49 | + def set_test_params(self): |
| 50 | + """Use the pregenerated, deterministic chain up to height 199.""" |
| 51 | + self.num_nodes = 3 |
| 52 | + self.rpc_timeout = 120 |
| 53 | + self.extra_args = [ |
| 54 | + [], |
| 55 | + ["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"], |
| 56 | + ["-txindex=1", "-blockfilterindex=1", "-coinstatsindex=1"], |
| 57 | + ] |
| 58 | + |
| 59 | + def setup_network(self): |
| 60 | + """Start with the nodes disconnected so that one can generate a snapshot |
| 61 | + including blocks the other hasn't yet seen.""" |
| 62 | + self.add_nodes(3) |
| 63 | + self.start_nodes(extra_args=self.extra_args) |
| 64 | + |
| 65 | + def run_test(self): |
| 66 | + """ |
| 67 | + Bring up two (disconnected) nodes, mine some new blocks on the first, |
| 68 | + and generate a UTXO snapshot. |
| 69 | +
|
| 70 | + Load the snapshot into the second, ensure it syncs to tip and completes |
| 71 | + background validation when connected to the first. |
| 72 | + """ |
| 73 | + n0 = self.nodes[0] |
| 74 | + n1 = self.nodes[1] |
| 75 | + n2 = self.nodes[2] |
| 76 | + |
| 77 | + # Mock time for a deterministic chain |
| 78 | + for n in self.nodes: |
| 79 | + n.setmocktime(n.getblockheader(n.getbestblockhash())['time']) |
| 80 | + |
| 81 | + self.sync_blocks() |
| 82 | + |
| 83 | + def no_sync(): |
| 84 | + pass |
| 85 | + |
| 86 | + # Generate a series of blocks that `n0` will have in the snapshot, |
| 87 | + # but that n1 doesn't yet see. In order for the snapshot to activate, |
| 88 | + # though, we have to ferry over the new headers to n1 so that it |
| 89 | + # isn't waiting forever to see the header of the snapshot's base block |
| 90 | + # while disconnected from n0. |
| 91 | + for i in range(100): |
| 92 | + self.generate(n0, nblocks=1, sync_fun=no_sync) |
| 93 | + newblock = n0.getblock(n0.getbestblockhash(), 0) |
| 94 | + |
| 95 | + # make n1 aware of the new header, but don't give it the block. |
| 96 | + n1.submitheader(newblock) |
| 97 | + n2.submitheader(newblock) |
| 98 | + |
| 99 | + # Ensure everyone is seeing the same headers. |
| 100 | + for n in self.nodes: |
| 101 | + assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT) |
| 102 | + |
| 103 | + self.log.info("-- Testing assumeutxo + some indexes + pruning") |
| 104 | + |
| 105 | + assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT) |
| 106 | + assert_equal(n1.getblockcount(), START_HEIGHT) |
| 107 | + |
| 108 | + self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}") |
| 109 | + dump_output = n0.dumptxoutset('utxos.dat') |
| 110 | + |
| 111 | + assert_equal( |
| 112 | + dump_output['txoutset_hash'], |
| 113 | + 'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0') |
| 114 | + assert_equal(dump_output['nchaintx'], 300) |
| 115 | + assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) |
| 116 | + |
| 117 | + # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This |
| 118 | + # will allow us to test n1's sync-to-tip on top of a snapshot. |
| 119 | + self.generate(n0, nblocks=100, sync_fun=no_sync) |
| 120 | + |
| 121 | + assert_equal(n0.getblockcount(), FINAL_HEIGHT) |
| 122 | + assert_equal(n1.getblockcount(), START_HEIGHT) |
| 123 | + |
| 124 | + assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT) |
| 125 | + |
| 126 | + self.log.info(f"Loading snapshot into second node from {dump_output['path']}") |
| 127 | + loaded = n1.loadtxoutset(dump_output['path']) |
| 128 | + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) |
| 129 | + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) |
| 130 | + |
| 131 | + monitor = n1.getchainstates() |
| 132 | + assert_equal(monitor['normal']['blocks'], START_HEIGHT) |
| 133 | + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) |
| 134 | + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) |
| 135 | + |
| 136 | + assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) |
| 137 | + |
| 138 | + PAUSE_HEIGHT = FINAL_HEIGHT - 40 |
| 139 | + |
| 140 | + self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT) |
| 141 | + self.restart_node(1, extra_args=[ |
| 142 | + f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]]) |
| 143 | + |
| 144 | + # Finally connect the nodes and let them sync. |
| 145 | + self.connect_nodes(0, 1) |
| 146 | + |
| 147 | + n1.wait_until_stopped(timeout=5) |
| 148 | + |
| 149 | + self.log.info("Checking that blocks are segmented on disk") |
| 150 | + assert self.has_blockfile(n1, "00000"), "normal blockfile missing" |
| 151 | + assert self.has_blockfile(n1, "00001"), "assumed blockfile missing" |
| 152 | + assert not self.has_blockfile(n1, "00002"), "too many blockfiles" |
| 153 | + |
| 154 | + self.log.info("Restarted node before snapshot validation completed, reloading...") |
| 155 | + self.restart_node(1, extra_args=self.extra_args[1]) |
| 156 | + self.connect_nodes(0, 1) |
| 157 | + |
| 158 | + self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") |
| 159 | + wait_until_helper(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) |
| 160 | + self.sync_blocks(nodes=(n0, n1)) |
| 161 | + |
| 162 | + self.log.info("Ensuring background validation completes") |
| 163 | + # N.B.: the `snapshot` key disappears once the background validation is complete. |
| 164 | + wait_until_helper(lambda: not n1.getchainstates().get('snapshot')) |
| 165 | + |
| 166 | + # Ensure indexes have synced. |
| 167 | + completed_idx_state = { |
| 168 | + 'basic block filter index': COMPLETE_IDX, |
| 169 | + 'coinstatsindex': COMPLETE_IDX, |
| 170 | + } |
| 171 | + self.wait_until(lambda: n1.getindexinfo() == completed_idx_state) |
| 172 | + |
| 173 | + |
| 174 | + for i in (0, 1): |
| 175 | + n = self.nodes[i] |
| 176 | + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") |
| 177 | + self.restart_node(i, extra_args=self.extra_args[i]) |
| 178 | + |
| 179 | + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) |
| 180 | + |
| 181 | + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) |
| 182 | + assert_equal(n.getchainstates().get('snapshot'), None) |
| 183 | + |
| 184 | + if i != 0: |
| 185 | + # Ensure indexes have synced for the assumeutxo node |
| 186 | + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) |
| 187 | + |
| 188 | + |
| 189 | + # Node 2: all indexes + reindex |
| 190 | + # ----------------------------- |
| 191 | + |
| 192 | + self.log.info("-- Testing all indexes + reindex") |
| 193 | + assert_equal(n2.getblockcount(), START_HEIGHT) |
| 194 | + |
| 195 | + self.log.info(f"Loading snapshot into third node from {dump_output['path']}") |
| 196 | + loaded = n2.loadtxoutset(dump_output['path']) |
| 197 | + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) |
| 198 | + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) |
| 199 | + |
| 200 | + monitor = n2.getchainstates() |
| 201 | + assert_equal(monitor['normal']['blocks'], START_HEIGHT) |
| 202 | + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) |
| 203 | + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) |
| 204 | + |
| 205 | + self.connect_nodes(0, 2) |
| 206 | + wait_until_helper(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) |
| 207 | + self.sync_blocks() |
| 208 | + |
| 209 | + self.log.info("Ensuring background validation completes") |
| 210 | + wait_until_helper(lambda: not n2.getchainstates().get('snapshot')) |
| 211 | + |
| 212 | + completed_idx_state = { |
| 213 | + 'basic block filter index': COMPLETE_IDX, |
| 214 | + 'coinstatsindex': COMPLETE_IDX, |
| 215 | + 'txindex': COMPLETE_IDX, |
| 216 | + } |
| 217 | + self.wait_until(lambda: n2.getindexinfo() == completed_idx_state) |
| 218 | + |
| 219 | + for i in (0, 2): |
| 220 | + n = self.nodes[i] |
| 221 | + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") |
| 222 | + self.restart_node(i, extra_args=self.extra_args[i]) |
| 223 | + |
| 224 | + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) |
| 225 | + |
| 226 | + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) |
| 227 | + assert_equal(n.getchainstates().get('snapshot'), None) |
| 228 | + |
| 229 | + if i != 0: |
| 230 | + # Ensure indexes have synced for the assumeutxo node |
| 231 | + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) |
| 232 | + |
| 233 | + self.log.info("Test -reindex-chainstate of an assumeutxo-synced node") |
| 234 | + self.restart_node(2, extra_args=[ |
| 235 | + '-reindex-chainstate=1', *self.extra_args[2]]) |
| 236 | + assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT) |
| 237 | + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) |
| 238 | + |
| 239 | + self.log.info("Test -reindex of an assumeutxo-synced node") |
| 240 | + self.restart_node(2, extra_args=['-reindex=1', *self.extra_args[2]]) |
| 241 | + self.connect_nodes(0, 2) |
| 242 | + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) |
| 243 | + |
| 244 | + |
| 245 | +if __name__ == '__main__': |
| 246 | + AssumeutxoTest().main() |
0 commit comments