Skip to content

Commit

Permalink
test: wallet change active chain during init
Browse files Browse the repository at this point in the history
  • Loading branch information
furszy committed Jan 23, 2025
1 parent 2c2dc92 commit 7fbbdbe
Showing 1 changed file with 82 additions and 7 deletions.
89 changes: 82 additions & 7 deletions test/functional/feature_chain_tiebreaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test that the correct active block is chosen in complex reorgs."""

from test_framework.blocktools import create_block
from test.functional.test_framework.util import assert_greater_than
from test_framework.address import address_to_scriptpubkey
from test_framework.blocktools import create_block, create_coinbase
from test_framework.messages import CBlockHeader
from test_framework.p2p import P2PDataStore
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal

class ChainTiebreaksTest(BitcoinTestFramework):
def add_options(self, parser):
self.add_wallet_options(parser)

def set_test_params(self):
self.num_nodes = 2
self.setup_clean_chain = True
self.extra_args = [['-txindex'], []]

@staticmethod
def send_headers(node, blocks):
Expand Down Expand Up @@ -104,6 +109,11 @@ def test_chain_split_in_memory(self):

def test_chain_split_from_disk(self):
node = self.nodes[0]
node.setmocktime(node.getblock(node.getblockhash(node.getblockcount()))["time"])

node.createwallet(wallet_name="wallet", load_on_startup=True)
wallet = node.get_wallet_rpc("wallet")

peer = node.add_p2p_connection(P2PDataStore())

self.log.info('Precomputing blocks')
Expand All @@ -121,12 +131,16 @@ def test_chain_split_from_disk(self):
genesis_block = node.getblock(node.getblockhash(start_height))
prev_time = genesis_block["time"]

coinbase_script = address_to_scriptpubkey(wallet.getnewaddress())
for i in range(0, 2):
prev_time = prev_time + i + 1
script_out = coinbase_script if i==0 else None
blocks.append(create_block(
hashprev=int(genesis_block["hash"], 16),
coinbase=create_coinbase(height=start_height + 1, script_pubkey=script_out),
tmpl={"height": start_height + 1,
# Make sure each block has a different hash.
"curtime": prev_time + i + 1,
"curtime": prev_time,
}
))
blocks[-1].solve()
Expand All @@ -135,15 +149,76 @@ def test_chain_split_from_disk(self):
self.log.info('Send A1 and A2. Make sure than only the former connects')
peer.send_blocks_and_test([blocks[0]], node, success=True)
peer.send_blocks_and_test([blocks[1]], node, success=False)
node.syncwithvalidationinterfacequeue()

# Verify we have balance.
assert_greater_than(wallet.getbalances()['mine']['immature'], 0)
# And verify we have two chains at the same height
assert all(chain['height'] == 1 for chain in node.getchaintips())

# Create a block on top of the prev_block_hash. block_height must be prev_block_height + 1.
def make_block(prev_block_hash, block_height, script_pubkey=None):
time = prev_time + 1 # Make sure each block has a different hash.
ret_block = create_block(
hashprev=prev_block_hash,
coinbase=create_coinbase(height=block_height, script_pubkey=script_pubkey),
tmpl={"height": block_height, "curtime": time}
)
ret_block.solve()
return ret_block

# Up to this point, there chain0 is the best chain. Both chains are at block height 1.
assert_equal(node.getbestblockhash(), blocks[0].hash)
# Now generate one more block for the second chain and make it the best chain.
chain1_block1 = blocks[1]
chain1_block2 = make_block(prev_block_hash=int(chain1_block1.hash, 16), block_height=start_height + 2)
peer.send_blocks_and_test([chain1_block2], node, success=True, timeout=10)
assert_equal(node.getbestblockhash(), chain1_block2.hash)

# Assert block0 is no longer part of the best chain
assert node.getblock(blocks[0].hash, verbose=1)['confirmations'] < 0

# As the second chain is our best chain now, the coinbase tx in the first chain should have been abandoned now.
assert_equal(wallet.gettransaction(blocks[0].vtx[0].hash)['details'][0]['abandoned'], True)

# Up this point, we are at chain1_block2, let's generate another block at chain0
chain0_block1 = blocks[0]
chain0_block2 = make_block(prev_block_hash=int(chain0_block1.hash, 16), block_height=start_height + 2)
peer.send_blocks_and_test([chain0_block2], node, success=False, timeout=10)

# Verify the active chain is still the chain2 block, and not this one
assert_equal(node.getbestblockhash(), chain1_block2.hash)
assert_equal(wallet.getwalletinfo()['lastprocessedblock']['hash'], chain1_block2.hash)
# Verify we have two chains at the same height
assert all(chain['height'] == 2 for chain in node.getchaintips())

# At this point, chain1 is the active chain, and it has the same amount of work than chain0.
# Restart node and see if chain0 becomes the active chain once more. And if that happens, create another
# block in the second chain to trigger another reorg and cause the crash.

self.log.info('Restart the node and check that the best tip before restarting matched the ones afterwards')
# Restart and check enough times to this to eventually fail if the logic is broken
for _ in range(10):
self.restart_node(0)
assert_equal(blocks[0].hash, node.getbestblockhash())
happened = False
for _ in range(15):
self.restart_node(0, extra_args=['-checkblocks=0'])
# Check the transaction is still abandoned upon restart
wallet = node.get_wallet_rpc("wallet")
assert_equal(wallet.gettransaction(blocks[0].vtx[0].hash)['details'][0]['abandoned'], True)
# If for some reason the first chain gets activated again, and become the main chain, this is an issue for the wallet
if node.getbestblockhash() == chain0_block2.hash:
# Make a block that will make the second chain the active chain again.
peer = node.add_p2p_connection(P2PDataStore())
prev_time = prev_time + 1
chain1_block3 = make_block(prev_block_hash=int(chain1_block2.hash, 16), block_height=start_height + 3)
peer.send_blocks_and_test([chain1_block3], node, success=True, timeout=10)
node.syncwithvalidationinterfacequeue()
assert False # no crash?

assert happened # the crash did not happen.


def run_test(self):
self.test_chain_split_in_memory()
#self.test_chain_split_in_memory()
self.test_chain_split_from_disk()


Expand Down

0 comments on commit 7fbbdbe

Please sign in to comment.