Skip to content

Commit

Permalink
[test] rescan legacy wallet with reorged parent + IsFromMe child in m…
Browse files Browse the repository at this point in the history
…empool

Test that wallet rescans process transactions topologically, even if a
parent's entry into the mempool is later than that of its child.
This behavior is important because IsFromMe requires the ability to look
up a transaction's inputs.

Github-Pull: #29179
Rebased-From: c3d02be
  • Loading branch information
glozow committed Jan 19, 2024
1 parent 438ac29 commit ecb8ebc
Showing 1 changed file with 47 additions and 9 deletions.
56 changes: 47 additions & 9 deletions test/functional/wallet_import_rescan.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
"""

from test_framework.test_framework import BitcoinTestFramework
from test_framework.address import AddressType
from test_framework.address import (
AddressType,
ADDRESS_BCRT1_UNSPENDABLE,
)
from test_framework.util import (
assert_equal,
set_node_times,
Expand Down Expand Up @@ -109,7 +112,7 @@ def check(self, txid=None, amount=None, confirmation_height=None):

address, = [ad for ad in addresses if txid in ad["txids"]]
assert_equal(address["address"], self.address["address"])
assert_equal(address["amount"], self.expected_balance)
assert_equal(address["amount"], self.amount_received)
assert_equal(address["confirmations"], confirmations)
# Verify the transaction is correctly marked watchonly depending on
# whether the transaction pays to an imported public key or
Expand Down Expand Up @@ -223,11 +226,11 @@ def run_test(self):
variant.node = self.nodes[2 + IMPORT_NODES.index(ImportNode(variant.prune, expect_rescan))]
variant.do_import(variant.timestamp)
if expect_rescan:
variant.expected_balance = variant.initial_amount
variant.amount_received = variant.initial_amount
variant.expected_txs = 1
variant.check(variant.initial_txid, variant.initial_amount, variant.confirmation_height)
else:
variant.expected_balance = 0
variant.amount_received = 0
variant.expected_txs = 0
variant.check()

Expand All @@ -247,7 +250,7 @@ def run_test(self):
# Check the latest results from getbalance and listtransactions.
for variant in IMPORT_VARIANTS:
self.log.info('Run check for variant {}'.format(variant))
variant.expected_balance += variant.sent_amount
variant.amount_received += variant.sent_amount
variant.expected_txs += 1
variant.check(variant.sent_txid, variant.sent_amount, variant.confirmation_height)

Expand All @@ -267,14 +270,45 @@ def run_test(self):
address_type=variant.address_type.value,
))
variant.key = self.nodes[1].dumpprivkey(variant.address["address"])
variant.initial_amount = get_rand_amount()
variant.initial_amount = get_rand_amount() * 2
variant.initial_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.initial_amount)
variant.confirmation_height = 0
variant.timestamp = timestamp

# Mine a block so these parents are confirmed
assert_equal(len(self.nodes[0].getrawmempool()), len(mempool_variants))
self.sync_mempools()
block_to_disconnect = self.generate(self.nodes[0], 1)[0]
assert_equal(len(self.nodes[0].getrawmempool()), 0)

# For each variant, create an unconfirmed child transaction from initial_txid, sending all
# the funds to an unspendable address. Importantly, no change output is created so the
# transaction can't be recognized using its outputs. The wallet rescan needs to know the
# inputs of the transaction to detect it, so the parent must be processed before the child.
# An equivalent test for descriptors exists in wallet_rescan_unconfirmed.py.
unspent_txid_map = {txin["txid"] : txin for txin in self.nodes[1].listunspent()}
for variant in mempool_variants:
# Send full amount, subtracting fee from outputs, to ensure no change is created.
child = self.nodes[1].send(
add_to_wallet=False,
inputs=[unspent_txid_map[variant.initial_txid]],
outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}],
subtract_fee_from_outputs=[0]
)
variant.child_txid = child["txid"]
variant.amount_received = 0
self.nodes[0].sendrawtransaction(child["hex"])

# Mempools should contain the child transactions for each variant.
assert_equal(len(self.nodes[0].getrawmempool()), len(mempool_variants))
self.sync_mempools()

# Mock a reorg so the parent transactions are added back to the mempool
for node in self.nodes:
node.invalidateblock(block_to_disconnect)
# Mempools should now contain the parent and child for each variant.
assert_equal(len(node.getrawmempool()), 2 * len(mempool_variants))

# For each variation of wallet key import, invoke the import RPC and
# check the results from getbalance and listtransactions.
for variant in mempool_variants:
Expand All @@ -283,11 +317,15 @@ def run_test(self):
variant.node = self.nodes[2 + IMPORT_NODES.index(ImportNode(variant.prune, expect_rescan))]
variant.do_import(variant.timestamp)
if expect_rescan:
variant.expected_balance = variant.initial_amount
# Ensure both transactions were rescanned. This would raise a JSONRPCError if the
# transactions were not identified as belonging to the wallet.
assert_equal(variant.node.gettransaction(variant.initial_txid)['confirmations'], 0)
assert_equal(variant.node.gettransaction(variant.child_txid)['confirmations'], 0)
variant.amount_received = variant.initial_amount
variant.expected_txs = 1
variant.check(variant.initial_txid, variant.initial_amount)
variant.check(variant.initial_txid, variant.initial_amount, 0)
else:
variant.expected_balance = 0
variant.amount_received = 0
variant.expected_txs = 0
variant.check()

Expand Down

0 comments on commit ecb8ebc

Please sign in to comment.