mirror of
https://github.com/bitcoin/bitcoin.git
synced 2024-11-19 18:09:47 +01:00
ccc431d53e
faf4315c88
test: Return dict in MiniWallet::send_to (MarcoFalke) Pull request description: Returning a tuple has many issues: * If only one value is needed, it can not be indexed by name * If another value is added to the return value, all call sites need to be updated Bite the bullet now and update all call sites to fix the above issues. ACKs for top commit: brunoerg: crACKfaf4315c88
theStack: Code-review ACKfaf4315c88
stickies-v: Code review ACKfaf4315c88
Tree-SHA512: 8ce1aca237df21f04b3990d0e5fcb49cc408fe6404399d3769a64eae1b5218941157d9785fce1bd9e45140cf70e06c3aa42646ee8f7b57855beb784fc3ef0261
325 lines
13 KiB
Python
Executable File
325 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2020-2022 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test coinstatsindex across nodes.
|
|
|
|
Test that the values returned by gettxoutsetinfo are consistent
|
|
between a node running the coinstatsindex and a node without
|
|
the index.
|
|
"""
|
|
|
|
from decimal import Decimal
|
|
|
|
from test_framework.blocktools import (
|
|
COINBASE_MATURITY,
|
|
create_block,
|
|
create_coinbase,
|
|
)
|
|
from test_framework.messages import (
|
|
COIN,
|
|
CTxOut,
|
|
)
|
|
from test_framework.script import (
|
|
CScript,
|
|
OP_FALSE,
|
|
OP_RETURN,
|
|
)
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_raises_rpc_error,
|
|
)
|
|
from test_framework.wallet import (
|
|
MiniWallet,
|
|
getnewdestination,
|
|
)
|
|
|
|
|
|
class CoinStatsIndexTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.setup_clean_chain = True
|
|
self.num_nodes = 2
|
|
self.supports_cli = False
|
|
self.extra_args = [
|
|
[],
|
|
["-coinstatsindex"]
|
|
]
|
|
|
|
def run_test(self):
|
|
self.wallet = MiniWallet(self.nodes[0])
|
|
self._test_coin_stats_index()
|
|
self._test_use_index_option()
|
|
self._test_reorg_index()
|
|
self._test_index_rejects_hash_serialized()
|
|
self._test_init_index_after_reorg()
|
|
|
|
def block_sanity_check(self, block_info):
|
|
block_subsidy = 50
|
|
assert_equal(
|
|
block_info['prevout_spent'] + block_subsidy,
|
|
block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable']
|
|
)
|
|
|
|
def sync_index_node(self):
|
|
self.wait_until(lambda: self.nodes[1].getindexinfo()['coinstatsindex']['synced'] is True)
|
|
|
|
def _test_coin_stats_index(self):
|
|
node = self.nodes[0]
|
|
index_node = self.nodes[1]
|
|
# Both none and muhash options allow the usage of the index
|
|
index_hash_options = ['none', 'muhash']
|
|
|
|
# Generate a normal transaction and mine it
|
|
self.generate(self.wallet, COINBASE_MATURITY + 1)
|
|
self.wallet.send_self_transfer(from_node=node)
|
|
self.generate(node, 1)
|
|
|
|
self.log.info("Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option")
|
|
res0 = node.gettxoutsetinfo('none')
|
|
|
|
# The fields 'disk_size' and 'transactions' do not exist on the index
|
|
del res0['disk_size'], res0['transactions']
|
|
|
|
for hash_option in index_hash_options:
|
|
res1 = index_node.gettxoutsetinfo(hash_option)
|
|
# The fields 'block_info' and 'total_unspendable_amount' only exist on the index
|
|
del res1['block_info'], res1['total_unspendable_amount']
|
|
res1.pop('muhash', None)
|
|
|
|
# Everything left should be the same
|
|
assert_equal(res1, res0)
|
|
|
|
self.log.info("Test that gettxoutsetinfo() can get fetch data on specific heights with index")
|
|
|
|
# Generate a new tip
|
|
self.generate(node, 5)
|
|
|
|
for hash_option in index_hash_options:
|
|
# Fetch old stats by height
|
|
res2 = index_node.gettxoutsetinfo(hash_option, 102)
|
|
del res2['block_info'], res2['total_unspendable_amount']
|
|
res2.pop('muhash', None)
|
|
assert_equal(res0, res2)
|
|
|
|
# Fetch old stats by hash
|
|
res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock'])
|
|
del res3['block_info'], res3['total_unspendable_amount']
|
|
res3.pop('muhash', None)
|
|
assert_equal(res0, res3)
|
|
|
|
# It does not work without coinstatsindex
|
|
assert_raises_rpc_error(-8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102)
|
|
|
|
self.log.info("Test gettxoutsetinfo() with index and verbose flag")
|
|
|
|
for hash_option in index_hash_options:
|
|
# Genesis block is unspendable
|
|
res4 = index_node.gettxoutsetinfo(hash_option, 0)
|
|
assert_equal(res4['total_unspendable_amount'], 50)
|
|
assert_equal(res4['block_info'], {
|
|
'unspendable': 50,
|
|
'prevout_spent': 0,
|
|
'new_outputs_ex_coinbase': 0,
|
|
'coinbase': 0,
|
|
'unspendables': {
|
|
'genesis_block': 50,
|
|
'bip30': 0,
|
|
'scripts': 0,
|
|
'unclaimed_rewards': 0
|
|
}
|
|
})
|
|
self.block_sanity_check(res4['block_info'])
|
|
|
|
# Test an older block height that included a normal tx
|
|
res5 = index_node.gettxoutsetinfo(hash_option, 102)
|
|
assert_equal(res5['total_unspendable_amount'], 50)
|
|
assert_equal(res5['block_info'], {
|
|
'unspendable': 0,
|
|
'prevout_spent': 50,
|
|
'new_outputs_ex_coinbase': Decimal('49.99968800'),
|
|
'coinbase': Decimal('50.00031200'),
|
|
'unspendables': {
|
|
'genesis_block': 0,
|
|
'bip30': 0,
|
|
'scripts': 0,
|
|
'unclaimed_rewards': 0,
|
|
}
|
|
})
|
|
self.block_sanity_check(res5['block_info'])
|
|
|
|
# Generate and send a normal tx with two outputs
|
|
tx1 = self.wallet.send_to(
|
|
from_node=node,
|
|
scriptPubKey=self.wallet.get_scriptPubKey(),
|
|
amount=21 * COIN,
|
|
)
|
|
|
|
# Find the right position of the 21 BTC output
|
|
tx1_out_21 = self.wallet.get_utxo(txid=tx1["txid"], vout=tx1["sent_vout"])
|
|
|
|
# Generate and send another tx with an OP_RETURN output (which is unspendable)
|
|
tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx']
|
|
tx2_val = '20.99'
|
|
tx2.vout = [CTxOut(int(Decimal(tx2_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
|
tx2_hex = tx2.serialize().hex()
|
|
self.nodes[0].sendrawtransaction(tx2_hex, 0, tx2_val)
|
|
|
|
# Include both txs in a block
|
|
self.generate(self.nodes[0], 1)
|
|
|
|
for hash_option in index_hash_options:
|
|
# Check all amounts were registered correctly
|
|
res6 = index_node.gettxoutsetinfo(hash_option, 108)
|
|
assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000'))
|
|
assert_equal(res6['block_info'], {
|
|
'unspendable': Decimal('20.99000000'),
|
|
'prevout_spent': 71,
|
|
'new_outputs_ex_coinbase': Decimal('49.99999000'),
|
|
'coinbase': Decimal('50.01001000'),
|
|
'unspendables': {
|
|
'genesis_block': 0,
|
|
'bip30': 0,
|
|
'scripts': Decimal('20.99000000'),
|
|
'unclaimed_rewards': 0,
|
|
}
|
|
})
|
|
self.block_sanity_check(res6['block_info'])
|
|
|
|
# Create a coinbase that does not claim full subsidy and also
|
|
# has two outputs
|
|
cb = create_coinbase(109, nValue=35)
|
|
cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE])))
|
|
cb.rehash()
|
|
|
|
# Generate a block that includes previous coinbase
|
|
tip = self.nodes[0].getbestblockhash()
|
|
block_time = self.nodes[0].getblock(tip)['time'] + 1
|
|
block = create_block(int(tip, 16), cb, block_time)
|
|
block.solve()
|
|
self.nodes[0].submitblock(block.serialize().hex())
|
|
self.sync_all()
|
|
|
|
for hash_option in index_hash_options:
|
|
res7 = index_node.gettxoutsetinfo(hash_option, 109)
|
|
assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000'))
|
|
assert_equal(res7['block_info'], {
|
|
'unspendable': 10,
|
|
'prevout_spent': 0,
|
|
'new_outputs_ex_coinbase': 0,
|
|
'coinbase': 40,
|
|
'unspendables': {
|
|
'genesis_block': 0,
|
|
'bip30': 0,
|
|
'scripts': 0,
|
|
'unclaimed_rewards': 10
|
|
}
|
|
})
|
|
self.block_sanity_check(res7['block_info'])
|
|
|
|
self.log.info("Test that the index is robust across restarts")
|
|
|
|
res8 = index_node.gettxoutsetinfo('muhash')
|
|
self.restart_node(1, extra_args=self.extra_args[1])
|
|
res9 = index_node.gettxoutsetinfo('muhash')
|
|
assert_equal(res8, res9)
|
|
|
|
self.generate(index_node, 1, sync_fun=self.no_op)
|
|
res10 = index_node.gettxoutsetinfo('muhash')
|
|
assert res8['txouts'] < res10['txouts']
|
|
|
|
self.log.info("Test that the index works with -reindex")
|
|
|
|
self.restart_node(1, extra_args=["-coinstatsindex", "-reindex"])
|
|
self.sync_index_node()
|
|
res11 = index_node.gettxoutsetinfo('muhash')
|
|
assert_equal(res11, res10)
|
|
|
|
self.log.info("Test that the index works with -reindex-chainstate")
|
|
|
|
self.restart_node(1, extra_args=["-coinstatsindex", "-reindex-chainstate"])
|
|
self.sync_index_node()
|
|
res12 = index_node.gettxoutsetinfo('muhash')
|
|
assert_equal(res12, res10)
|
|
|
|
def _test_use_index_option(self):
|
|
self.log.info("Test use_index option for nodes running the index")
|
|
|
|
self.connect_nodes(0, 1)
|
|
self.nodes[0].waitforblockheight(110)
|
|
res = self.nodes[0].gettxoutsetinfo('muhash')
|
|
option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
|
|
del res['disk_size'], option_res['disk_size']
|
|
assert_equal(res, option_res)
|
|
|
|
def _test_reorg_index(self):
|
|
self.log.info("Test that index can handle reorgs")
|
|
|
|
# Generate two block, let the index catch up, then invalidate the blocks
|
|
index_node = self.nodes[1]
|
|
reorg_blocks = self.generatetoaddress(index_node, 2, getnewdestination()[2])
|
|
reorg_block = reorg_blocks[1]
|
|
self.sync_index_node()
|
|
res_invalid = index_node.gettxoutsetinfo('muhash')
|
|
index_node.invalidateblock(reorg_blocks[0])
|
|
assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110)
|
|
|
|
# Add two new blocks
|
|
block = self.generate(index_node, 2, sync_fun=self.no_op)[1]
|
|
res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
|
|
|
|
# Test that the result of the reorged block is not returned for its old block height
|
|
res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
|
|
assert_equal(res["bestblock"], block)
|
|
assert_equal(res["muhash"], res2["muhash"])
|
|
assert res["muhash"] != res_invalid["muhash"]
|
|
|
|
# Test that requesting reorged out block by hash is still returning correct results
|
|
res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block)
|
|
assert_equal(res_invalid2["muhash"], res_invalid["muhash"])
|
|
assert res["muhash"] != res_invalid2["muhash"]
|
|
|
|
# Add another block, so we don't depend on reconsiderblock remembering which
|
|
# blocks were touched by invalidateblock
|
|
self.generate(index_node, 1)
|
|
|
|
# Ensure that removing and re-adding blocks yields consistent results
|
|
block = index_node.getblockhash(99)
|
|
index_node.invalidateblock(block)
|
|
index_node.reconsiderblock(block)
|
|
res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
|
|
assert_equal(res2, res3)
|
|
|
|
def _test_index_rejects_hash_serialized(self):
|
|
self.log.info("Test that the rpc raises if the legacy hash is passed with the index")
|
|
|
|
msg = "hash_serialized_2 hash type cannot be queried for a specific block"
|
|
assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111)
|
|
|
|
for use_index in {True, False, None}:
|
|
assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111, use_index=use_index)
|
|
|
|
def _test_init_index_after_reorg(self):
|
|
self.log.info("Test a reorg while the index is deactivated")
|
|
index_node = self.nodes[1]
|
|
block = self.nodes[0].getbestblockhash()
|
|
self.generate(index_node, 2, sync_fun=self.no_op)
|
|
self.sync_index_node()
|
|
|
|
# Restart without index
|
|
self.restart_node(1, extra_args=[])
|
|
self.connect_nodes(0, 1)
|
|
index_node.invalidateblock(block)
|
|
self.generatetoaddress(index_node, 5, getnewdestination()[2])
|
|
res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
|
|
|
|
# Restart with index that still has its best block on the old chain
|
|
self.restart_node(1, extra_args=self.extra_args[1])
|
|
self.sync_index_node()
|
|
res1 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=True)
|
|
assert_equal(res["muhash"], res1["muhash"])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
CoinStatsIndexTest().main()
|