/ test / functional / feature_coinstatsindex.py
feature_coinstatsindex.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2020-2022 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 coinstatsindex across nodes.
  6  
  7  Test that the values returned by gettxoutsetinfo are consistent
  8  between a node running the coinstatsindex and a node without
  9  the index.
 10  """
 11  
 12  from decimal import Decimal
 13  
 14  from test_framework.blocktools import (
 15      COINBASE_MATURITY,
 16      create_block,
 17      create_coinbase,
 18  )
 19  from test_framework.messages import (
 20      COIN,
 21      CTxOut,
 22  )
 23  from test_framework.script import (
 24      CScript,
 25      OP_FALSE,
 26      OP_RETURN,
 27  )
 28  from test_framework.test_framework import BitcoinTestFramework
 29  from test_framework.util import (
 30      assert_equal,
 31      assert_raises_rpc_error,
 32  )
 33  from test_framework.wallet import (
 34      MiniWallet,
 35      getnewdestination,
 36  )
 37  
 38  
 39  class CoinStatsIndexTest(BitcoinTestFramework):
 40      def set_test_params(self):
 41          self.setup_clean_chain = True
 42          self.num_nodes = 2
 43          self.supports_cli = False
 44          self.extra_args = [
 45              [],
 46              ["-coinstatsindex"]
 47          ]
 48  
 49      def run_test(self):
 50          self.wallet = MiniWallet(self.nodes[0])
 51          self._test_coin_stats_index()
 52          self._test_use_index_option()
 53          self._test_reorg_index()
 54          self._test_index_rejects_hash_serialized()
 55          self._test_init_index_after_reorg()
 56  
 57      def block_sanity_check(self, block_info):
 58          block_subsidy = 50
 59          assert_equal(
 60              block_info['prevout_spent'] + block_subsidy,
 61              block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable']
 62          )
 63  
 64      def sync_index_node(self):
 65          self.wait_until(lambda: self.nodes[1].getindexinfo()['coinstatsindex']['synced'] is True)
 66  
 67      def _test_coin_stats_index(self):
 68          node = self.nodes[0]
 69          index_node = self.nodes[1]
 70          # Both none and muhash options allow the usage of the index
 71          index_hash_options = ['none', 'muhash']
 72  
 73          # Generate a normal transaction and mine it
 74          self.generate(self.wallet, COINBASE_MATURITY + 1)
 75          self.wallet.send_self_transfer(from_node=node)
 76          self.generate(node, 1)
 77  
 78          self.log.info("Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option")
 79          res0 = node.gettxoutsetinfo('none')
 80  
 81          # The fields 'disk_size' and 'transactions' do not exist on the index
 82          del res0['disk_size'], res0['transactions']
 83  
 84          for hash_option in index_hash_options:
 85              res1 = index_node.gettxoutsetinfo(hash_option)
 86              # The fields 'block_info' and 'total_unspendable_amount' only exist on the index
 87              del res1['block_info'], res1['total_unspendable_amount']
 88              res1.pop('muhash', None)
 89  
 90              # Everything left should be the same
 91              assert_equal(res1, res0)
 92  
 93          self.log.info("Test that gettxoutsetinfo() can get fetch data on specific heights with index")
 94  
 95          # Generate a new tip
 96          self.generate(node, 5)
 97  
 98          for hash_option in index_hash_options:
 99              # Fetch old stats by height
100              res2 = index_node.gettxoutsetinfo(hash_option, 102)
101              del res2['block_info'], res2['total_unspendable_amount']
102              res2.pop('muhash', None)
103              assert_equal(res0, res2)
104  
105              # Fetch old stats by hash
106              res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock'])
107              del res3['block_info'], res3['total_unspendable_amount']
108              res3.pop('muhash', None)
109              assert_equal(res0, res3)
110  
111              # It does not work without coinstatsindex
112              assert_raises_rpc_error(-8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102)
113  
114          self.log.info("Test gettxoutsetinfo() with index and verbose flag")
115  
116          for hash_option in index_hash_options:
117              # Genesis block is unspendable
118              res4 = index_node.gettxoutsetinfo(hash_option, 0)
119              assert_equal(res4['total_unspendable_amount'], 50)
120              assert_equal(res4['block_info'], {
121                  'unspendable': 50,
122                  'prevout_spent': 0,
123                  'new_outputs_ex_coinbase': 0,
124                  'coinbase': 0,
125                  'unspendables': {
126                      'genesis_block': 50,
127                      'bip30': 0,
128                      'scripts': 0,
129                      'unclaimed_rewards': 0
130                  }
131              })
132              self.block_sanity_check(res4['block_info'])
133  
134              # Test an older block height that included a normal tx
135              res5 = index_node.gettxoutsetinfo(hash_option, 102)
136              assert_equal(res5['total_unspendable_amount'], 50)
137              assert_equal(res5['block_info'], {
138                  'unspendable': 0,
139                  'prevout_spent': 50,
140                  'new_outputs_ex_coinbase': Decimal('49.99968800'),
141                  'coinbase': Decimal('50.00031200'),
142                  'unspendables': {
143                      'genesis_block': 0,
144                      'bip30': 0,
145                      'scripts': 0,
146                      'unclaimed_rewards': 0,
147                  }
148              })
149              self.block_sanity_check(res5['block_info'])
150  
151          # Generate and send a normal tx with two outputs
152          tx1 = self.wallet.send_to(
153              from_node=node,
154              scriptPubKey=self.wallet.get_scriptPubKey(),
155              amount=21 * COIN,
156          )
157  
158          # Find the right position of the 21 BTC output
159          tx1_out_21 = self.wallet.get_utxo(txid=tx1["txid"], vout=tx1["sent_vout"])
160  
161          # Generate and send another tx with an OP_RETURN output (which is unspendable)
162          tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx']
163          tx2_val = '20.99'
164          tx2.vout = [CTxOut(int(Decimal(tx2_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
165          tx2_hex = tx2.serialize().hex()
166          self.nodes[0].sendrawtransaction(tx2_hex, 0, tx2_val)
167  
168          # Include both txs in a block
169          self.generate(self.nodes[0], 1)
170  
171          for hash_option in index_hash_options:
172              # Check all amounts were registered correctly
173              res6 = index_node.gettxoutsetinfo(hash_option, 108)
174              assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000'))
175              assert_equal(res6['block_info'], {
176                  'unspendable': Decimal('20.99000000'),
177                  'prevout_spent': 71,
178                  'new_outputs_ex_coinbase': Decimal('49.99999000'),
179                  'coinbase': Decimal('50.01001000'),
180                  'unspendables': {
181                      'genesis_block': 0,
182                      'bip30': 0,
183                      'scripts': Decimal('20.99000000'),
184                      'unclaimed_rewards': 0,
185                  }
186              })
187              self.block_sanity_check(res6['block_info'])
188  
189          # Create a coinbase that does not claim full subsidy and also
190          # has two outputs
191          cb = create_coinbase(109, nValue=35)
192          cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE])))
193          cb.rehash()
194  
195          # Generate a block that includes previous coinbase
196          tip = self.nodes[0].getbestblockhash()
197          block_time = self.nodes[0].getblock(tip)['time'] + 1
198          block = create_block(int(tip, 16), cb, block_time)
199          block.solve()
200          self.nodes[0].submitblock(block.serialize().hex())
201          self.sync_all()
202  
203          for hash_option in index_hash_options:
204              res7 = index_node.gettxoutsetinfo(hash_option, 109)
205              assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000'))
206              assert_equal(res7['block_info'], {
207                  'unspendable': 10,
208                  'prevout_spent': 0,
209                  'new_outputs_ex_coinbase': 0,
210                  'coinbase': 40,
211                  'unspendables': {
212                      'genesis_block': 0,
213                      'bip30': 0,
214                      'scripts': 0,
215                      'unclaimed_rewards': 10
216                  }
217              })
218              self.block_sanity_check(res7['block_info'])
219  
220          self.log.info("Test that the index is robust across restarts")
221  
222          res8 = index_node.gettxoutsetinfo('muhash')
223          self.restart_node(1, extra_args=self.extra_args[1])
224          res9 = index_node.gettxoutsetinfo('muhash')
225          assert_equal(res8, res9)
226  
227          self.generate(index_node, 1, sync_fun=self.no_op)
228          res10 = index_node.gettxoutsetinfo('muhash')
229          assert res8['txouts'] < res10['txouts']
230  
231          self.log.info("Test that the index works with -reindex")
232  
233          self.restart_node(1, extra_args=["-coinstatsindex", "-reindex"])
234          self.sync_index_node()
235          res11 = index_node.gettxoutsetinfo('muhash')
236          assert_equal(res11, res10)
237  
238          self.log.info("Test that the index works with -reindex-chainstate")
239  
240          self.restart_node(1, extra_args=["-coinstatsindex", "-reindex-chainstate"])
241          self.sync_index_node()
242          res12 = index_node.gettxoutsetinfo('muhash')
243          assert_equal(res12, res10)
244  
245      def _test_use_index_option(self):
246          self.log.info("Test use_index option for nodes running the index")
247  
248          self.connect_nodes(0, 1)
249          self.nodes[0].waitforblockheight(110)
250          res = self.nodes[0].gettxoutsetinfo('muhash')
251          option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
252          del res['disk_size'], option_res['disk_size']
253          assert_equal(res, option_res)
254  
255      def _test_reorg_index(self):
256          self.log.info("Test that index can handle reorgs")
257  
258          # Generate two block, let the index catch up, then invalidate the blocks
259          index_node = self.nodes[1]
260          reorg_blocks = self.generatetoaddress(index_node, 2, getnewdestination()[2])
261          reorg_block = reorg_blocks[1]
262          self.sync_index_node()
263          res_invalid = index_node.gettxoutsetinfo('muhash')
264          index_node.invalidateblock(reorg_blocks[0])
265          assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110)
266  
267          # Add two new blocks
268          block = self.generate(index_node, 2, sync_fun=self.no_op)[1]
269          res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
270  
271          # Test that the result of the reorged block is not returned for its old block height
272          res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
273          assert_equal(res["bestblock"], block)
274          assert_equal(res["muhash"], res2["muhash"])
275          assert res["muhash"] != res_invalid["muhash"]
276  
277          # Test that requesting reorged out block by hash is still returning correct results
278          res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block)
279          assert_equal(res_invalid2["muhash"], res_invalid["muhash"])
280          assert res["muhash"] != res_invalid2["muhash"]
281  
282          # Add another block, so we don't depend on reconsiderblock remembering which
283          # blocks were touched by invalidateblock
284          self.generate(index_node, 1)
285  
286          # Ensure that removing and re-adding blocks yields consistent results
287          block = index_node.getblockhash(99)
288          index_node.invalidateblock(block)
289          index_node.reconsiderblock(block)
290          res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
291          assert_equal(res2, res3)
292  
293      def _test_index_rejects_hash_serialized(self):
294          self.log.info("Test that the rpc raises if the legacy hash is passed with the index")
295  
296          msg = "hash_serialized_3 hash type cannot be queried for a specific block"
297          assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111)
298  
299          for use_index in {True, False, None}:
300              assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111, use_index=use_index)
301  
302      def _test_init_index_after_reorg(self):
303          self.log.info("Test a reorg while the index is deactivated")
304          index_node = self.nodes[1]
305          block = self.nodes[0].getbestblockhash()
306          self.generate(index_node, 2, sync_fun=self.no_op)
307          self.sync_index_node()
308  
309          # Restart without index
310          self.restart_node(1, extra_args=[])
311          self.connect_nodes(0, 1)
312          index_node.invalidateblock(block)
313          self.generatetoaddress(index_node, 5, getnewdestination()[2])
314          res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
315  
316          # Restart with index that still has its best block on the old chain
317          self.restart_node(1, extra_args=self.extra_args[1])
318          self.sync_index_node()
319          res1 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=True)
320          assert_equal(res["muhash"], res1["muhash"])
321  
322  
323  if __name__ == '__main__':
324      CoinStatsIndexTest().main()