mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-13 03:09:37 +01:00
168 lines
8.5 KiB
Python
Executable file
168 lines
8.5 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2019-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 tx status in case of reorgs while wallet being shutdown.
|
|
|
|
Wallet txn status rely on block connection/disconnection for its
|
|
accuracy. In case of reorgs happening while wallet being shutdown
|
|
block updates are not going to be received. At wallet loading, we
|
|
check against chain if confirmed txn are still in chain and change
|
|
their status if block in which they have been included has been
|
|
disconnected.
|
|
"""
|
|
|
|
from decimal import Decimal
|
|
import shutil
|
|
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_raises_rpc_error
|
|
)
|
|
|
|
class ReorgsRestoreTest(BitcoinTestFramework):
|
|
def add_options(self, parser):
|
|
self.add_wallet_options(parser)
|
|
|
|
def set_test_params(self):
|
|
self.num_nodes = 3
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def test_coinbase_automatic_abandon_during_startup(self):
|
|
##########################################################################################################
|
|
# Verify the wallet marks coinbase transactions, and their descendants, as abandoned during startup when #
|
|
# the block is no longer part of the best chain. #
|
|
##########################################################################################################
|
|
self.log.info("Test automatic coinbase abandonment during startup")
|
|
# Test setup: Sync nodes for the coming test, ensuring both are at the same block, then disconnect them to
|
|
# generate two competing chains. After disconnection, verify no other peer connection exists.
|
|
self.connect_nodes(1, 0)
|
|
self.sync_blocks(self.nodes[:2])
|
|
self.disconnect_nodes(1, 0)
|
|
assert all(len(node.getpeerinfo()) == 0 for node in self.nodes[:2])
|
|
|
|
# Create a new block in node0, coinbase going to wallet0
|
|
self.nodes[0].createwallet(wallet_name="w0", load_on_startup=True)
|
|
wallet0 = self.nodes[0].get_wallet_rpc("w0")
|
|
self.generatetoaddress(self.nodes[0], 1, wallet0.getnewaddress(), sync_fun=self.no_op)
|
|
node0_coinbase_tx_hash = wallet0.getblock(wallet0.getbestblockhash(), verbose=1)['tx'][0]
|
|
|
|
# Mine 100 blocks on top to mature the coinbase and create a descendant
|
|
self.generate(self.nodes[0], 101, sync_fun=self.no_op)
|
|
# Make descendant, send-to-self
|
|
descendant_tx_id = wallet0.sendtoaddress(wallet0.getnewaddress(), 1)
|
|
|
|
# Verify balance
|
|
wallet0.syncwithvalidationinterfacequeue()
|
|
assert(wallet0.getbalances()['mine']['trusted'] > 0)
|
|
|
|
# Now create a fork in node1. This will be used to replace node0's chain later.
|
|
self.nodes[1].createwallet(wallet_name="w1", load_on_startup=True)
|
|
wallet1 = self.nodes[1].get_wallet_rpc("w1")
|
|
self.generatetoaddress(self.nodes[1], 1, wallet1.getnewaddress(), sync_fun=self.no_op)
|
|
wallet1.syncwithvalidationinterfacequeue()
|
|
|
|
# Verify both nodes are on a different chain
|
|
block0_best_hash, block1_best_hash = wallet0.getbestblockhash(), wallet1.getbestblockhash()
|
|
assert(block0_best_hash != block1_best_hash)
|
|
|
|
# Stop both nodes and replace node0 chain entirely for the node1 chain
|
|
self.stop_nodes()
|
|
for path in ["chainstate", "blocks"]:
|
|
shutil.rmtree(self.nodes[0].chain_path / path)
|
|
shutil.copytree(self.nodes[1].chain_path / path, self.nodes[0].chain_path / path)
|
|
|
|
# Start node0 and verify that now it has node1 chain and no info about its previous best block
|
|
self.start_node(0)
|
|
wallet0 = self.nodes[0].get_wallet_rpc("w0")
|
|
assert_equal(wallet0.getbestblockhash(), block1_best_hash)
|
|
assert_raises_rpc_error(-5, "Block not found", wallet0.getblock, block0_best_hash)
|
|
|
|
# Verify the coinbase tx was marked as abandoned and balance correctly computed
|
|
tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0]
|
|
assert_equal(tx_info['abandoned'], True)
|
|
assert_equal(tx_info['category'], 'orphan')
|
|
assert(wallet0.getbalances()['mine']['trusted'] == 0)
|
|
# Verify the coinbase descendant was also marked as abandoned
|
|
assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)
|
|
|
|
|
|
def run_test(self):
|
|
# Send a tx from which to conflict outputs later
|
|
txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
|
|
self.generate(self.nodes[0], 1)
|
|
|
|
# Disconnect node1 from others to reorg its chain later
|
|
self.disconnect_nodes(0, 1)
|
|
self.disconnect_nodes(1, 2)
|
|
self.connect_nodes(0, 2)
|
|
|
|
# Send a tx to be unconfirmed later
|
|
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
|
|
tx = self.nodes[0].gettransaction(txid)
|
|
self.generate(self.nodes[0], 4, sync_fun=self.no_op)
|
|
self.sync_blocks([self.nodes[0], self.nodes[2]])
|
|
tx_before_reorg = self.nodes[0].gettransaction(txid)
|
|
assert_equal(tx_before_reorg["confirmations"], 4)
|
|
|
|
# Disconnect node0 from node2 to broadcast a conflict on their respective chains
|
|
self.disconnect_nodes(0, 2)
|
|
nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10"))
|
|
inputs = []
|
|
inputs.append({"txid": txid_conflict_from, "vout": nA})
|
|
outputs_1 = {}
|
|
outputs_2 = {}
|
|
|
|
# Create a conflicted tx broadcast on node0 chain and conflicting tx broadcast on node1 chain. Both spend from txid_conflict_from
|
|
outputs_1[self.nodes[0].getnewaddress()] = Decimal("9.99998")
|
|
outputs_2[self.nodes[0].getnewaddress()] = Decimal("9.99998")
|
|
conflicted = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_1))
|
|
conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2))
|
|
|
|
conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"])
|
|
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
|
|
conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"])
|
|
self.generate(self.nodes[2], 9, sync_fun=self.no_op)
|
|
|
|
# Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted
|
|
self.connect_nodes(0, 2)
|
|
self.sync_blocks([self.nodes[0], self.nodes[2]])
|
|
conflicted = self.nodes[0].gettransaction(conflicted_txid)
|
|
conflicting = self.nodes[0].gettransaction(conflicting_txid)
|
|
assert_equal(conflicted["confirmations"], -9)
|
|
assert_equal(conflicted["walletconflicts"][0], conflicting["txid"])
|
|
|
|
# Node0 wallet is shutdown
|
|
self.restart_node(0)
|
|
|
|
# The block chain re-orgs and the tx is included in a different block
|
|
self.generate(self.nodes[1], 9, sync_fun=self.no_op)
|
|
self.nodes[1].sendrawtransaction(tx["hex"])
|
|
self.generate(self.nodes[1], 1, sync_fun=self.no_op)
|
|
self.nodes[1].sendrawtransaction(conflicted["hex"])
|
|
self.generate(self.nodes[1], 1, sync_fun=self.no_op)
|
|
|
|
# Node0 wallet file is loaded on longest sync'ed node1
|
|
self.stop_node(1)
|
|
self.nodes[0].backupwallet(self.nodes[0].datadir_path / 'wallet.bak')
|
|
shutil.copyfile(self.nodes[0].datadir_path / 'wallet.bak', self.nodes[1].chain_path / self.default_wallet_name / self.wallet_data_filename)
|
|
self.start_node(1)
|
|
tx_after_reorg = self.nodes[1].gettransaction(txid)
|
|
# Check that normal confirmed tx is confirmed again but with different blockhash
|
|
assert_equal(tx_after_reorg["confirmations"], 2)
|
|
assert tx_before_reorg["blockhash"] != tx_after_reorg["blockhash"]
|
|
conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid)
|
|
# Check that conflicted tx is confirmed again with blockhash different than previously conflicting tx
|
|
assert_equal(conflicted_after_reorg["confirmations"], 1)
|
|
assert conflicting["blockhash"] != conflicted_after_reorg["blockhash"]
|
|
|
|
# Verify we mark coinbase txs, and their descendants, as abandoned during startup
|
|
self.test_coinbase_automatic_abandon_during_startup()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ReorgsRestoreTest(__file__).main()
|