Skip to content

Commit 50f250a

Browse files
committed
Merge bitcoin/bitcoin#28542: wallet: Check for uninitialized last processed and conflicting heights in MarkConflicted
782701c test: Test loading wallets with conflicts without a chain (Andrew Chow) 4660fc8 wallet: Check last block and conflict height are valid in MarkConflicted (Andrew Chow) Pull request description: `MarkConflicted` assumes that `m_last_block_processed_height` is always valid. However it may not be valid when a chain is not attached, as happens in the wallet tool and during migration. In such situations, when the conflicting height is also negative (which occurs on loading when no chain is available), the calculation of the number of conflict confirms results in a non-negative value which passes the existing check for valid values. This will subsequently hit an assertion in `GetTxDepthInMainChain`. Furthermore, `MarkConflicted` is also only called on loading a transaction whose parent has a stored state of `TxStateConflicted` and was loaded before the child transaction. This depends on the loading order, which for both sqlite and bdb depends on the txids. We can avoid this by explicitly checking that both `m_last_block_processed_height` and `conflicting_height` are non-negative. Both `tool_wallet.py` and `wallet_migration.py` are updated to create wallets with a state that triggers the assertion. Fixes #28510 ACKs for top commit: ryanofsky: Code review ACK 782701c. Nice catch, and clever test (grinding the txid) furszy: ACK 782701c Tree-SHA512: 1344e0279ec5413a43a2819d101fb571fbf4821de2d13958a0fdffc99f57082ef3243ec454c8343f97dc02ed1fce8c8b0fd89388420ab2e55618af42ad5630a9
2 parents dcf6230 + 782701c commit 50f250a

File tree

3 files changed

+105
-1
lines changed

3 files changed

+105
-1
lines changed

src/wallet/wallet.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -1339,11 +1339,14 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
13391339
{
13401340
LOCK(cs_wallet);
13411341

1342-
int conflictconfirms = (m_last_block_processed_height - conflicting_height + 1) * -1;
13431342
// If number of conflict confirms cannot be determined, this means
13441343
// that the block is still unknown or not yet part of the main chain,
13451344
// for example when loading the wallet during a reindex. Do nothing in that
13461345
// case.
1346+
if (m_last_block_processed_height < 0 || conflicting_height < 0) {
1347+
return;
1348+
}
1349+
int conflictconfirms = (m_last_block_processed_height - conflicting_height + 1) * -1;
13471350
if (conflictconfirms >= 0)
13481351
return;
13491352

test/functional/tool_wallet.py

+57
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,62 @@ def test_dump_createfromdump(self):
394394
self.assert_raises_tool_error('Error: Checksum is not the correct size', '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump')
395395
assert not (self.nodes[0].wallets_path / "badload").is_dir()
396396

397+
def test_chainless_conflicts(self):
398+
self.log.info("Test wallet tool when wallet contains conflicting transactions")
399+
self.restart_node(0)
400+
self.generate(self.nodes[0], 101)
401+
402+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
403+
404+
self.nodes[0].createwallet("conflicts")
405+
wallet = self.nodes[0].get_wallet_rpc("conflicts")
406+
def_wallet.sendtoaddress(wallet.getnewaddress(), 10)
407+
self.generate(self.nodes[0], 1)
408+
409+
# parent tx
410+
parent_txid = wallet.sendtoaddress(wallet.getnewaddress(), 9)
411+
parent_txid_bytes = bytes.fromhex(parent_txid)[::-1]
412+
conflict_utxo = wallet.gettransaction(txid=parent_txid, verbose=True)["decoded"]["vin"][0]
413+
414+
# The specific assertion in MarkConflicted being tested requires that the parent tx is already loaded
415+
# by the time the child tx is loaded. Since transactions end up being loaded in txid order due to how both
416+
# and sqlite store things, we can just grind the child tx until it has a txid that is greater than the parent's.
417+
locktime = 500000000 # Use locktime as nonce, starting at unix timestamp minimum
418+
addr = wallet.getnewaddress()
419+
while True:
420+
child_send_res = wallet.send(outputs=[{addr: 8}], add_to_wallet=False, locktime=locktime)
421+
child_txid = child_send_res["txid"]
422+
child_txid_bytes = bytes.fromhex(child_txid)[::-1]
423+
if (child_txid_bytes > parent_txid_bytes):
424+
wallet.sendrawtransaction(child_send_res["hex"])
425+
break
426+
locktime += 1
427+
428+
# conflict with parent
429+
conflict_unsigned = self.nodes[0].createrawtransaction(inputs=[conflict_utxo], outputs=[{wallet.getnewaddress(): 9.9999}])
430+
conflict_signed = wallet.signrawtransactionwithwallet(conflict_unsigned)["hex"]
431+
conflict_txid = self.nodes[0].sendrawtransaction(conflict_signed)
432+
self.generate(self.nodes[0], 1)
433+
assert_equal(wallet.gettransaction(txid=parent_txid)["confirmations"], -1)
434+
assert_equal(wallet.gettransaction(txid=child_txid)["confirmations"], -1)
435+
assert_equal(wallet.gettransaction(txid=conflict_txid)["confirmations"], 1)
436+
437+
self.stop_node(0)
438+
439+
# Wallet tool should successfully give info for this wallet
440+
expected_output = textwrap.dedent(f'''\
441+
Wallet info
442+
===========
443+
Name: conflicts
444+
Format: {"sqlite" if self.options.descriptors else "bdb"}
445+
Descriptors: {"yes" if self.options.descriptors else "no"}
446+
Encrypted: no
447+
HD (hd seed available): yes
448+
Keypool Size: {"8" if self.options.descriptors else "1"}
449+
Transactions: 4
450+
Address Book: 4
451+
''')
452+
self.assert_tool_output(expected_output, "-wallet=conflicts", "info")
397453

398454
def run_test(self):
399455
self.wallet_path = os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename)
@@ -407,6 +463,7 @@ def run_test(self):
407463
# Salvage is a legacy wallet only thing
408464
self.test_salvage()
409465
self.test_dump_createfromdump()
466+
self.test_chainless_conflicts()
410467

411468
if __name__ == '__main__':
412469
ToolWalletTest().main()

test/functional/wallet_migration.py

+44
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,49 @@ def send_to_script(script, amount):
727727
self.nodes[0].loadwallet(info_migration["watchonly_name"])
728728
assert_equal(wallet_wo.getbalances()['mine']['trusted'], 5)
729729

730+
def test_conflict_txs(self):
731+
self.log.info("Test migration when wallet contains conflicting transactions")
732+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
733+
734+
wallet = self.create_legacy_wallet("conflicts")
735+
def_wallet.sendtoaddress(wallet.getnewaddress(), 10)
736+
self.generate(self.nodes[0], 1)
737+
738+
# parent tx
739+
parent_txid = wallet.sendtoaddress(wallet.getnewaddress(), 9)
740+
parent_txid_bytes = bytes.fromhex(parent_txid)[::-1]
741+
conflict_utxo = wallet.gettransaction(txid=parent_txid, verbose=True)["decoded"]["vin"][0]
742+
743+
# The specific assertion in MarkConflicted being tested requires that the parent tx is already loaded
744+
# by the time the child tx is loaded. Since transactions end up being loaded in txid order due to how both
745+
# and sqlite store things, we can just grind the child tx until it has a txid that is greater than the parent's.
746+
locktime = 500000000 # Use locktime as nonce, starting at unix timestamp minimum
747+
addr = wallet.getnewaddress()
748+
while True:
749+
child_send_res = wallet.send(outputs=[{addr: 8}], add_to_wallet=False, locktime=locktime)
750+
child_txid = child_send_res["txid"]
751+
child_txid_bytes = bytes.fromhex(child_txid)[::-1]
752+
if (child_txid_bytes > parent_txid_bytes):
753+
wallet.sendrawtransaction(child_send_res["hex"])
754+
break
755+
locktime += 1
756+
757+
# conflict with parent
758+
conflict_unsigned = self.nodes[0].createrawtransaction(inputs=[conflict_utxo], outputs=[{wallet.getnewaddress(): 9.9999}])
759+
conflict_signed = wallet.signrawtransactionwithwallet(conflict_unsigned)["hex"]
760+
conflict_txid = self.nodes[0].sendrawtransaction(conflict_signed)
761+
self.generate(self.nodes[0], 1)
762+
assert_equal(wallet.gettransaction(txid=parent_txid)["confirmations"], -1)
763+
assert_equal(wallet.gettransaction(txid=child_txid)["confirmations"], -1)
764+
assert_equal(wallet.gettransaction(txid=conflict_txid)["confirmations"], 1)
765+
766+
wallet.migratewallet()
767+
assert_equal(wallet.gettransaction(txid=parent_txid)["confirmations"], -1)
768+
assert_equal(wallet.gettransaction(txid=child_txid)["confirmations"], -1)
769+
assert_equal(wallet.gettransaction(txid=conflict_txid)["confirmations"], 1)
770+
771+
wallet.unloadwallet()
772+
730773
def run_test(self):
731774
self.generate(self.nodes[0], 101)
732775

@@ -743,6 +786,7 @@ def run_test(self):
743786
self.test_direct_file()
744787
self.test_addressbook()
745788
self.test_migrate_raw_p2sh()
789+
self.test_conflict_txs()
746790

747791
if __name__ == '__main__':
748792
WalletMigrationTest().main()

0 commit comments

Comments
 (0)