Changeset View
Changeset View
Standalone View
Standalone View
test/functional/wallet_abandonconflict.py
Show All 26 Lines | class AbandonConflictTest(BitcoinTestFramework): | ||||
def skip_test_if_missing_module(self): | def skip_test_if_missing_module(self): | ||||
self.skip_if_no_wallet() | self.skip_if_no_wallet() | ||||
def run_test(self): | def run_test(self): | ||||
def total_fees(*txids): | def total_fees(*txids): | ||||
total = 0 | total = 0 | ||||
for txid in txids: | for txid in txids: | ||||
# '-=' is because gettransaction(txid)['fee'] returns a negative | # '-=' is because gettransaction(txid)['fee'] returns a negative | ||||
total -= self.nodes[0].gettransaction(txid)['fee'] | total -= self.nodes[0].gettransaction(txid)["fee"] | ||||
return satoshi_round(total) | return satoshi_round(total) | ||||
self.generate(self.nodes[1], 100) | self.generate(self.nodes[1], 100) | ||||
balance = self.nodes[0].getbalance() | balance = self.nodes[0].getbalance() | ||||
txA = self.nodes[0].sendtoaddress( | txA = self.nodes[0].sendtoaddress( | ||||
self.nodes[0].getnewaddress(), Decimal("10000000")) | self.nodes[0].getnewaddress(), Decimal("10000000") | ||||
) | |||||
txB = self.nodes[0].sendtoaddress( | txB = self.nodes[0].sendtoaddress( | ||||
self.nodes[0].getnewaddress(), Decimal("10000000")) | self.nodes[0].getnewaddress(), Decimal("10000000") | ||||
) | |||||
txC = self.nodes[0].sendtoaddress( | txC = self.nodes[0].sendtoaddress( | ||||
self.nodes[0].getnewaddress(), Decimal("10000000")) | self.nodes[0].getnewaddress(), Decimal("10000000") | ||||
) | |||||
self.sync_mempools() | self.sync_mempools() | ||||
self.generate(self.nodes[1], 1) | self.generate(self.nodes[1], 1) | ||||
# Can not abandon non-wallet transaction | # Can not abandon non-wallet transaction | ||||
assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', | assert_raises_rpc_error( | ||||
lambda: self.nodes[0].abandontransaction(txid='ff' * 32)) | -5, | ||||
"Invalid or non-wallet transaction id", | |||||
lambda: self.nodes[0].abandontransaction(txid="ff" * 32), | |||||
) | |||||
# Can not abandon confirmed transaction | # Can not abandon confirmed transaction | ||||
assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', | assert_raises_rpc_error( | ||||
lambda: self.nodes[0].abandontransaction(txid=txA)) | -5, | ||||
"Transaction not eligible for abandonment", | |||||
lambda: self.nodes[0].abandontransaction(txid=txA), | |||||
) | |||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
# no more than fees lost | # no more than fees lost | ||||
assert balance - newbalance <= total_fees(txA, txB, txC) | assert balance - newbalance <= total_fees(txA, txB, txC) | ||||
balance = newbalance | balance = newbalance | ||||
# Disconnect nodes so node0's transactions don't get into node1's | # Disconnect nodes so node0's transactions don't get into node1's | ||||
# mempool | # mempool | ||||
self.disconnect_nodes(0, 1) | self.disconnect_nodes(0, 1) | ||||
# Identify the 10btc outputs | # Identify the 10btc outputs | ||||
nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( | nA = next( | ||||
txA)["details"] if tx_out["amount"] == Decimal("10000000")) | tx_out["vout"] | ||||
nB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( | for tx_out in self.nodes[0].gettransaction(txA)["details"] | ||||
txB)["details"] if tx_out["amount"] == Decimal("10000000")) | if tx_out["amount"] == Decimal("10000000") | ||||
nC = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( | ) | ||||
txC)["details"] if tx_out["amount"] == Decimal("10000000")) | nB = next( | ||||
tx_out["vout"] | |||||
for tx_out in self.nodes[0].gettransaction(txB)["details"] | |||||
if tx_out["amount"] == Decimal("10000000") | |||||
) | |||||
nC = next( | |||||
tx_out["vout"] | |||||
for tx_out in self.nodes[0].gettransaction(txC)["details"] | |||||
if tx_out["amount"] == Decimal("10000000") | |||||
) | |||||
inputs = [] | inputs = [] | ||||
# spend 10btc outputs from txA and txB | # spend 10btc outputs from txA and txB | ||||
inputs.append({"txid": txA, "vout": nA}) | inputs.append({"txid": txA, "vout": nA}) | ||||
inputs.append({"txid": txB, "vout": nB}) | inputs.append({"txid": txB, "vout": nB}) | ||||
outputs = {} | outputs = {} | ||||
outputs[self.nodes[0].getnewaddress()] = Decimal("14999980") | outputs[self.nodes[0].getnewaddress()] = Decimal("14999980") | ||||
outputs[self.nodes[1].getnewaddress()] = Decimal("5000000") | outputs[self.nodes[1].getnewaddress()] = Decimal("5000000") | ||||
signed = self.nodes[0].signrawtransactionwithwallet( | signed = self.nodes[0].signrawtransactionwithwallet( | ||||
self.nodes[0].createrawtransaction(inputs, outputs)) | self.nodes[0].createrawtransaction(inputs, outputs) | ||||
) | |||||
txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) | txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) | ||||
# Identify the 14,999,980 XEC output | # Identify the 14,999,980 XEC output | ||||
nAB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( | nAB = next( | ||||
txAB1)["details"] if tx_out["amount"] == Decimal("14999980")) | tx_out["vout"] | ||||
for tx_out in self.nodes[0].gettransaction(txAB1)["details"] | |||||
if tx_out["amount"] == Decimal("14999980") | |||||
) | |||||
# Create a child tx spending AB1 and C | # Create a child tx spending AB1 and C | ||||
inputs = [] | inputs = [] | ||||
# Amount 14,999,980 XEC | # Amount 14,999,980 XEC | ||||
inputs.append({"txid": txAB1, "vout": nAB}) | inputs.append({"txid": txAB1, "vout": nAB}) | ||||
# Amount 10,000,000 XEC | # Amount 10,000,000 XEC | ||||
inputs.append({"txid": txC, "vout": nC}) | inputs.append({"txid": txC, "vout": nC}) | ||||
outputs = {} | outputs = {} | ||||
outputs[self.nodes[0].getnewaddress()] = Decimal("24999600") | outputs[self.nodes[0].getnewaddress()] = Decimal("24999600") | ||||
signed2 = self.nodes[0].signrawtransactionwithwallet( | signed2 = self.nodes[0].signrawtransactionwithwallet( | ||||
self.nodes[0].createrawtransaction(inputs, outputs)) | self.nodes[0].createrawtransaction(inputs, outputs) | ||||
) | |||||
txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) | txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) | ||||
# Create a child tx spending ABC2 | # Create a child tx spending ABC2 | ||||
signed3_change = Decimal("24999000") | signed3_change = Decimal("24999000") | ||||
inputs = [{"txid": txABC2, "vout": 0}] | inputs = [{"txid": txABC2, "vout": 0}] | ||||
outputs = {self.nodes[0].getnewaddress(): signed3_change} | outputs = {self.nodes[0].getnewaddress(): signed3_change} | ||||
signed3 = self.nodes[0].signrawtransactionwithwallet( | signed3 = self.nodes[0].signrawtransactionwithwallet( | ||||
self.nodes[0].createrawtransaction(inputs, outputs)) | self.nodes[0].createrawtransaction(inputs, outputs) | ||||
) | |||||
# note tx is never directly referenced, only abandoned as a child of | # note tx is never directly referenced, only abandoned as a child of | ||||
# the above | # the above | ||||
self.nodes[0].sendrawtransaction(signed3["hex"]) | self.nodes[0].sendrawtransaction(signed3["hex"]) | ||||
# In mempool txs from self should increase balance from change | # In mempool txs from self should increase balance from change | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal( | assert_equal(newbalance, balance - Decimal("30000000") + signed3_change) | ||||
newbalance, | |||||
balance - | |||||
Decimal("30000000") + | |||||
signed3_change) | |||||
balance = newbalance | balance = newbalance | ||||
# Restart the node with a higher min relay fee so the parent tx is no longer in mempool | # Restart the node with a higher min relay fee so the parent tx is no longer in mempool | ||||
# TODO: redo with eviction | # TODO: redo with eviction | ||||
self.restart_node(0, extra_args=["-minrelaytxfee=100"]) | self.restart_node(0, extra_args=["-minrelaytxfee=100"]) | ||||
assert self.nodes[0].getmempoolinfo()['loaded'] | assert self.nodes[0].getmempoolinfo()["loaded"] | ||||
# Verify txs no longer in either node's mempool | # Verify txs no longer in either node's mempool | ||||
assert_equal(len(self.nodes[0].getrawmempool()), 0) | assert_equal(len(self.nodes[0].getrawmempool()), 0) | ||||
assert_equal(len(self.nodes[1].getrawmempool()), 0) | assert_equal(len(self.nodes[1].getrawmempool()), 0) | ||||
# Transactions which are not in the mempool should only reduce wallet balance. | # Transactions which are not in the mempool should only reduce wallet balance. | ||||
# Transaction inputs should still be spent, but the change not yet | # Transaction inputs should still be spent, but the change not yet | ||||
# received. | # received. | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal(newbalance, balance - signed3_change) | assert_equal(newbalance, balance - signed3_change) | ||||
# Unconfirmed received funds that are not in mempool also shouldn't show | # Unconfirmed received funds that are not in mempool also shouldn't show | ||||
# up in unconfirmed balance. Note that the transactions stored in the wallet | # up in unconfirmed balance. Note that the transactions stored in the wallet | ||||
# are not necessarily in the node's mempool. | # are not necessarily in the node's mempool. | ||||
balances = self.nodes[0].getbalances()['mine'] | balances = self.nodes[0].getbalances()["mine"] | ||||
assert_equal( | assert_equal(balances["untrusted_pending"] + balances["trusted"], newbalance) | ||||
balances['untrusted_pending'] + | |||||
balances['trusted'], | |||||
newbalance) | |||||
# Unconfirmed transactions which are not in the mempool should also | # Unconfirmed transactions which are not in the mempool should also | ||||
# not be in listunspent | # not be in listunspent | ||||
assert txABC2 not in [utxo["txid"] | assert txABC2 not in [utxo["txid"] for utxo in self.nodes[0].listunspent(0)] | ||||
for utxo in self.nodes[0].listunspent(0)] | |||||
balance = newbalance | balance = newbalance | ||||
# Abandon original transaction and verify inputs are available again | # Abandon original transaction and verify inputs are available again | ||||
# including that the child tx was also abandoned | # including that the child tx was also abandoned | ||||
self.nodes[0].abandontransaction(txAB1) | self.nodes[0].abandontransaction(txAB1) | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal(newbalance, balance + Decimal("30000000")) | assert_equal(newbalance, balance + Decimal("30000000")) | ||||
balance = newbalance | balance = newbalance | ||||
self.log.info("Check abandoned transactions in listsinceblock") | self.log.info("Check abandoned transactions in listsinceblock") | ||||
listsinceblock = self.nodes[0].listsinceblock() | listsinceblock = self.nodes[0].listsinceblock() | ||||
txAB1_listsinceblock = [d for d in listsinceblock['transactions'] | txAB1_listsinceblock = [ | ||||
if d['txid'] == txAB1 and d['category'] == 'send'] | d | ||||
for d in listsinceblock["transactions"] | |||||
if d["txid"] == txAB1 and d["category"] == "send" | |||||
] | |||||
for tx in txAB1_listsinceblock: | for tx in txAB1_listsinceblock: | ||||
assert_equal(tx['abandoned'], True) | assert_equal(tx["abandoned"], True) | ||||
assert_equal(tx['confirmations'], 0) | assert_equal(tx["confirmations"], 0) | ||||
assert_equal(tx['trusted'], False) | assert_equal(tx["trusted"], False) | ||||
# Verify that even with a low min relay fee, the tx is not re-accepted | # Verify that even with a low min relay fee, the tx is not re-accepted | ||||
# from wallet on startup once abandoned. | # from wallet on startup once abandoned. | ||||
self.restart_node(0, extra_args=["-minrelaytxfee=10"]) | self.restart_node(0, extra_args=["-minrelaytxfee=10"]) | ||||
assert self.nodes[0].getmempoolinfo()['loaded'] | assert self.nodes[0].getmempoolinfo()["loaded"] | ||||
assert_equal(len(self.nodes[0].getrawmempool()), 0) | assert_equal(len(self.nodes[0].getrawmempool()), 0) | ||||
assert_equal(self.nodes[0].getbalance(), balance) | assert_equal(self.nodes[0].getbalance(), balance) | ||||
# If the transaction is re-sent the wallet also unabandons it. The | # If the transaction is re-sent the wallet also unabandons it. The | ||||
# change should be available, and it's child transaction should remain | # change should be available, and it's child transaction should remain | ||||
# abandoned. | # abandoned. | ||||
# NOTE: Abandoned transactions are internal to the wallet, and tracked | # NOTE: Abandoned transactions are internal to the wallet, and tracked | ||||
# separately from other indices. | # separately from other indices. | ||||
self.nodes[0].sendrawtransaction(signed["hex"]) | self.nodes[0].sendrawtransaction(signed["hex"]) | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal( | assert_equal(newbalance, balance - Decimal("20000000") + Decimal("14999980")) | ||||
newbalance, | |||||
balance - | |||||
Decimal("20000000") + | |||||
Decimal("14999980")) | |||||
balance = newbalance | balance = newbalance | ||||
# Send child tx again so it is no longer abandoned. | # Send child tx again so it is no longer abandoned. | ||||
self.nodes[0].sendrawtransaction(signed2["hex"]) | self.nodes[0].sendrawtransaction(signed2["hex"]) | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal(newbalance, balance - Decimal("10000000") | assert_equal( | ||||
- Decimal("14999980") + Decimal("24999600")) | newbalance, | ||||
balance - Decimal("10000000") - Decimal("14999980") + Decimal("24999600"), | |||||
) | |||||
balance = newbalance | balance = newbalance | ||||
# Reset to a higher relay fee so that we abandon a transaction | # Reset to a higher relay fee so that we abandon a transaction | ||||
self.restart_node(0, extra_args=["-minrelaytxfee=100"]) | self.restart_node(0, extra_args=["-minrelaytxfee=100"]) | ||||
assert self.nodes[0].getmempoolinfo()['loaded'] | assert self.nodes[0].getmempoolinfo()["loaded"] | ||||
assert_equal(len(self.nodes[0].getrawmempool()), 0) | assert_equal(len(self.nodes[0].getrawmempool()), 0) | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
assert_equal(newbalance, balance - Decimal("24999600")) | assert_equal(newbalance, balance - Decimal("24999600")) | ||||
balance = newbalance | balance = newbalance | ||||
# Create a double spend of AB1. Spend it again from only A's 10 output. | # Create a double spend of AB1. Spend it again from only A's 10 output. | ||||
# Mine double spend from node 1. | # Mine double spend from node 1. | ||||
inputs = [] | inputs = [] | ||||
Show All 17 Lines | def run_test(self): | ||||
# There is currently a minor bug around this and so this test doesn't | # There is currently a minor bug around this and so this test doesn't | ||||
# work. See Issue #7315 | # work. See Issue #7315 | ||||
# Invalidate the block with the double spend and B's 10,000,000 XEC | # Invalidate the block with the double spend and B's 10,000,000 XEC | ||||
# output should no longer be available. Don't think C's should either | # output should no longer be available. Don't think C's should either | ||||
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) | self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) | ||||
newbalance = self.nodes[0].getbalance() | newbalance = self.nodes[0].getbalance() | ||||
# assert_equal(newbalance, balance - Decimal("10000000")) | # assert_equal(newbalance, balance - Decimal("10000000")) | ||||
self.log.info( | self.log.info( | ||||
"If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer") | "If balance has not declined after invalidateblock then out of mempool" | ||||
" wallet tx which is no longer" | |||||
) | |||||
self.log.info( | self.log.info( | ||||
"conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315") | "conflicted has not resumed causing its inputs to be seen as spent. See" | ||||
" Issue #7315" | |||||
) | |||||
self.log.info(f"{str(balance)} -> {str(newbalance)} ?") | self.log.info(f"{str(balance)} -> {str(newbalance)} ?") | ||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
AbandonConflictTest().main() | AbandonConflictTest().main() |