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