1 #!/usr/bin/env python3
2 # Copyright (c) 2014-present The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Test the abandontransaction RPC.
6 7 The abandontransaction RPC marks a transaction and all its in-wallet
8 descendants as abandoned which allows their inputs to be respent. It can be
9 used to replace "stuck" or evicted transactions. It only works on transactions
10 which are not included in a block and are not currently in the mempool. It has
11 no effect on transactions which are already abandoned.
12 """
13 from decimal import Decimal
14 15 from test_framework.blocktools import COINBASE_MATURITY
16 from test_framework.test_framework import BitcoinTestFramework
17 from test_framework.util import (
18 assert_equal,
19 assert_raises_rpc_error,
20 )
21 22 23 class AbandonConflictTest(BitcoinTestFramework):
24 def set_test_params(self):
25 self.num_nodes = 2
26 self.extra_args = [["-minrelaytxfee=0.00001"], []]
27 # whitelist peers to speed up tx relay / mempool sync
28 self.noban_tx_relay = True
29 30 def skip_test_if_missing_module(self):
31 self.skip_if_no_wallet()
32 33 def run_test(self):
34 # create two wallets to tests conflicts from both sender's and receiver's sides
35 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
36 self.nodes[0].createwallet(wallet_name="bob")
37 bob = self.nodes[0].get_wallet_rpc("bob")
38 39 self.generate(self.nodes[1], COINBASE_MATURITY)
40 balance = alice.getbalance()
41 txA = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
42 txB = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
43 txC = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
44 self.sync_mempools()
45 46 # Can not abandon transaction in mempool
47 assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))
48 49 self.generate(self.nodes[1], 1)
50 51 # Can not abandon non-wallet transaction
52 assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: alice.abandontransaction(txid='ff' * 32))
53 # Can not abandon confirmed transaction
54 assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))
55 56 newbalance = alice.getbalance()
57 assert balance - newbalance < Decimal("0.001") #no more than fees lost
58 balance = newbalance
59 60 # Disconnect nodes so node0's transactions don't get into node1's mempool
61 self.disconnect_nodes(0, 1)
62 63 # Identify the 10btc outputs
64 nA = next(tx_out["vout"] for tx_out in alice.gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10"))
65 nB = next(tx_out["vout"] for tx_out in alice.gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10"))
66 nC = next(tx_out["vout"] for tx_out in alice.gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10"))
67 68 inputs = []
69 # spend 10btc outputs from txA and txB
70 inputs.append({"txid": txA, "vout": nA})
71 inputs.append({"txid": txB, "vout": nB})
72 outputs = {}
73 74 outputs[alice.getnewaddress()] = Decimal("14.99998")
75 outputs[bob.getnewaddress()] = Decimal("5")
76 signed = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
77 txAB1 = self.nodes[0].sendrawtransaction(signed["hex"])
78 79 # Identify the 14.99998btc output
80 nAB = next(tx_out["vout"] for tx_out in alice.gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998"))
81 82 #Create a child tx spending AB1 and C
83 inputs = []
84 inputs.append({"txid": txAB1, "vout": nAB})
85 inputs.append({"txid": txC, "vout": nC})
86 outputs = {}
87 outputs[alice.getnewaddress()] = Decimal("24.9996")
88 signed2 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
89 txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"])
90 91 # Create a child tx spending ABC2
92 signed3_change = Decimal("24.999")
93 inputs = [{"txid": txABC2, "vout": 0}]
94 outputs = {alice.getnewaddress(): signed3_change}
95 signed3 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
96 # note tx is never directly referenced, only abandoned as a child of the above
97 self.nodes[0].sendrawtransaction(signed3["hex"])
98 99 # In mempool txs from self should increase balance from change
100 newbalance = alice.getbalance()
101 assert_equal(newbalance, balance - Decimal("30") + signed3_change)
102 balance = newbalance
103 104 # Restart the node with a higher min relay fee so the parent tx is no longer in mempool
105 # TODO: redo with eviction
106 self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"])
107 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
108 assert self.nodes[0].getmempoolinfo()['loaded']
109 110 # Verify txs no longer in either node's mempool
111 assert_equal(len(self.nodes[0].getrawmempool()), 0)
112 assert_equal(len(self.nodes[1].getrawmempool()), 0)
113 114 # Not in mempool txs from self should only reduce balance
115 # inputs are still spent, but change not received
116 newbalance = alice.getbalance()
117 assert_equal(newbalance, balance - signed3_change)
118 # Unconfirmed received funds that are not in mempool
119 balances = alice.getbalances()['mine']
120 assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance)
121 # Also shouldn't show up in listunspent
122 assert txABC2 not in [utxo["txid"] for utxo in alice.listunspent(0)]
123 balance = newbalance
124 125 # Abandon original transaction and verify inputs are available again
126 # including that the child tx was also abandoned
127 alice.abandontransaction(txAB1)
128 newbalance = alice.getbalance()
129 assert_equal(newbalance, balance + Decimal("30"))
130 balance = newbalance
131 132 self.log.info("Check abandoned transactions in listsinceblock")
133 listsinceblock = alice.listsinceblock()
134 txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send']
135 for tx in txAB1_listsinceblock:
136 assert_equal(tx['abandoned'], True)
137 assert_equal(tx['confirmations'], 0)
138 assert_equal(tx['trusted'], False)
139 140 # Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned
141 self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"])
142 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
143 assert self.nodes[0].getmempoolinfo()['loaded']
144 145 assert_equal(len(self.nodes[0].getrawmempool()), 0)
146 assert_equal(alice.getbalance(), balance)
147 148 # But if it is received again then it is unabandoned
149 # And since now in mempool, the change is available
150 # But its child tx remains abandoned
151 self.nodes[0].sendrawtransaction(signed["hex"])
152 newbalance = alice.getbalance()
153 assert_equal(newbalance, balance - Decimal("20") + Decimal("14.99998"))
154 balance = newbalance
155 156 # Send child tx again so it is unabandoned
157 self.nodes[0].sendrawtransaction(signed2["hex"])
158 newbalance = alice.getbalance()
159 assert_equal(newbalance, balance - Decimal("10") - Decimal("14.99998") + Decimal("24.9996"))
160 balance = newbalance
161 162 # Remove using high relay fee again
163 self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"])
164 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
165 assert self.nodes[0].getmempoolinfo()['loaded']
166 assert_equal(len(self.nodes[0].getrawmempool()), 0)
167 newbalance = alice.getbalance()
168 assert_equal(newbalance, balance - Decimal("24.9996"))
169 balance = newbalance
170 171 self.log.info("Test transactions conflicted by a double spend")
172 self.nodes[0].loadwallet("bob")
173 bob = self.nodes[0].get_wallet_rpc("bob")
174 175 # Create a double spend of AB1 by spending again from only A's 10 output
176 # Mine double spend from node 1
177 inputs = []
178 inputs.append({"txid": txA, "vout": nA})
179 outputs = {}
180 outputs[self.nodes[1].getnewaddress()] = Decimal("3.9999")
181 outputs[bob.getnewaddress()] = Decimal("5.9999")
182 tx = alice.createrawtransaction(inputs, outputs)
183 signed = alice.signrawtransactionwithwallet(tx)
184 double_spend_txid = self.nodes[1].sendrawtransaction(signed["hex"])
185 self.connect_nodes(0, 1)
186 self.generate(self.nodes[1], 1)
187 188 tx_list = alice.listtransactions()
189 190 conflicted = [tx for tx in tx_list if tx["confirmations"] < 0]
191 assert_equal(4, len(conflicted))
192 193 wallet_conflicts = [tx for tx in conflicted if tx["walletconflicts"]]
194 assert_equal(2, len(wallet_conflicts))
195 196 double_spends = [tx for tx in tx_list if tx["walletconflicts"] and tx["confirmations"] > 0]
197 assert_equal(2, len(double_spends)) # one for each output
198 double_spend = double_spends[0]
199 200 # Test the properties of the conflicted transactions, i.e. with confirmations < 0.
201 for tx in conflicted:
202 assert_equal(tx["abandoned"], False)
203 assert_equal(tx["confirmations"], -1)
204 assert_equal(tx["trusted"], False)
205 206 # Test the properties of the double-spend transaction, i.e. having wallet conflicts and confirmations > 0.
207 assert_equal(double_spend["abandoned"], False)
208 assert_equal(double_spend["confirmations"], 1)
209 assert "trusted" not in double_spend.keys() # "trusted" only returned if tx has 0 or negative confirmations.
210 211 # Test the walletconflicts field of each.
212 for tx in wallet_conflicts:
213 assert_equal(double_spend["walletconflicts"], [tx["txid"]])
214 assert_equal(tx["walletconflicts"], [double_spend["txid"]])
215 216 # Test walletconflicts on the receiver's side
217 txinfo = bob.gettransaction(txAB1)
218 assert_equal(txinfo['confirmations'], -1)
219 assert_equal(txinfo['walletconflicts'], [double_spend['txid']])
220 221 double_spends = [tx for tx in bob.listtransactions() if tx["walletconflicts"] and tx["confirmations"] > 0]
222 assert_equal(1, len(double_spends))
223 double_spend = double_spends[0]
224 assert_equal(double_spend_txid, double_spend['txid'])
225 assert_equal(double_spend["walletconflicts"], [txAB1])
226 227 # Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted
228 assert_equal(alice.gettransaction(txAB1)["confirmations"], -1)
229 newbalance = alice.getbalance()
230 assert_equal(newbalance, balance + Decimal("20"))
231 balance = newbalance
232 233 # Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available
234 blk = self.nodes[0].getbestblockhash()
235 # mine 10 blocks so that when the blk is invalidated, the transactions are not
236 # returned to the mempool
237 self.generate(self.nodes[1], 10)
238 self.nodes[0].invalidateblock(blk)
239 assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)
240 newbalance = alice.getbalance()
241 assert_equal(newbalance, balance - Decimal("20"))
242 243 if __name__ == '__main__':
244 AbandonConflictTest(__file__).main()
245