Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2014-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test the abandontransaction RPC.7The abandontransaction RPC marks a transaction and all its in-wallet8descendants as abandoned which allows their inputs to be respent. It can be9used to replace "stuck" or evicted transactions. It only works on transactions10which are not included in a block and are not currently in the mempool. It has11no effect on transactions which are already abandoned.12"""13from decimal import Decimal15from test_framework.blocktools import COINBASE_MATURITY16from test_framework.test_framework import BitcoinTestFramework17from test_framework.util import (18assert_equal,19assert_raises_rpc_error,20)23class AbandonConflictTest(BitcoinTestFramework):24def set_test_params(self):25self.num_nodes = 226self.extra_args = [["-minrelaytxfee=0.00001"], []]27# whitelist peers to speed up tx relay / mempool sync28self.noban_tx_relay = True30def skip_test_if_missing_module(self):31self.skip_if_no_wallet()33def run_test(self):34# create two wallets to tests conflicts from both sender's and receiver's sides35alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)36self.nodes[0].createwallet(wallet_name="bob")37bob = self.nodes[0].get_wallet_rpc("bob")39self.generate(self.nodes[1], COINBASE_MATURITY)40balance = alice.getbalance()41txA = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))42txB = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))43txC = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))44self.sync_mempools()46# Can not abandon transaction in mempool47assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))49self.generate(self.nodes[1], 1)51# Can not abandon non-wallet transaction52assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: alice.abandontransaction(txid='ff' * 32))53# Can not abandon confirmed transaction54assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))56newbalance = alice.getbalance()57assert balance - newbalance < Decimal("0.001") #no more than fees lost58balance = newbalance60# Disconnect nodes so node0's transactions don't get into node1's mempool61self.disconnect_nodes(0, 1)63# Identify the 10btc outputs64nA = next(tx_out["vout"] for tx_out in alice.gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10"))65nB = next(tx_out["vout"] for tx_out in alice.gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10"))66nC = next(tx_out["vout"] for tx_out in alice.gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10"))68inputs = []69# spend 10btc outputs from txA and txB70inputs.append({"txid": txA, "vout": nA})71inputs.append({"txid": txB, "vout": nB})72outputs = {}74outputs[alice.getnewaddress()] = Decimal("14.99998")75outputs[bob.getnewaddress()] = Decimal("5")76signed = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))77txAB1 = self.nodes[0].sendrawtransaction(signed["hex"])79# Identify the 14.99998btc output80nAB = next(tx_out["vout"] for tx_out in alice.gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998"))82#Create a child tx spending AB1 and C83inputs = []84inputs.append({"txid": txAB1, "vout": nAB})85inputs.append({"txid": txC, "vout": nC})86outputs = {}87outputs[alice.getnewaddress()] = Decimal("24.9996")88signed2 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))89txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"])91# Create a child tx spending ABC292signed3_change = Decimal("24.999")93inputs = [{"txid": txABC2, "vout": 0}]94outputs = {alice.getnewaddress(): signed3_change}95signed3 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))96# note tx is never directly referenced, only abandoned as a child of the above97self.nodes[0].sendrawtransaction(signed3["hex"])99# In mempool txs from self should increase balance from change100newbalance = alice.getbalance()101assert_equal(newbalance, balance - Decimal("30") + signed3_change)102balance = newbalance104# Restart the node with a higher min relay fee so the parent tx is no longer in mempool105# TODO: redo with eviction106self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"])107alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)108assert self.nodes[0].getmempoolinfo()['loaded']110# Verify txs no longer in either node's mempool111assert_equal(len(self.nodes[0].getrawmempool()), 0)112assert_equal(len(self.nodes[1].getrawmempool()), 0)114# Not in mempool txs from self should only reduce balance115# inputs are still spent, but change not received116newbalance = alice.getbalance()117assert_equal(newbalance, balance - signed3_change)118# Unconfirmed received funds that are not in mempool119balances = alice.getbalances()['mine']120assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance)121# Also shouldn't show up in listunspent122assert txABC2 not in [utxo["txid"] for utxo in alice.listunspent(0)]123balance = newbalance125# Abandon original transaction and verify inputs are available again126# including that the child tx was also abandoned127alice.abandontransaction(txAB1)128newbalance = alice.getbalance()129assert_equal(newbalance, balance + Decimal("30"))130balance = newbalance132self.log.info("Check abandoned transactions in listsinceblock")133listsinceblock = alice.listsinceblock()134txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send']135for tx in txAB1_listsinceblock:136assert_equal(tx['abandoned'], True)137assert_equal(tx['confirmations'], 0)138assert_equal(tx['trusted'], False)140# Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned141self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"])142alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)143assert self.nodes[0].getmempoolinfo()['loaded']145assert_equal(len(self.nodes[0].getrawmempool()), 0)146assert_equal(alice.getbalance(), balance)148# But if it is received again then it is unabandoned149# And since now in mempool, the change is available150# But its child tx remains abandoned151self.nodes[0].sendrawtransaction(signed["hex"])152newbalance = alice.getbalance()153assert_equal(newbalance, balance - Decimal("20") + Decimal("14.99998"))154balance = newbalance156# Send child tx again so it is unabandoned157self.nodes[0].sendrawtransaction(signed2["hex"])158newbalance = alice.getbalance()159assert_equal(newbalance, balance - Decimal("10") - Decimal("14.99998") + Decimal("24.9996"))160balance = newbalance162# Remove using high relay fee again163self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"])164alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)165assert self.nodes[0].getmempoolinfo()['loaded']166assert_equal(len(self.nodes[0].getrawmempool()), 0)167newbalance = alice.getbalance()168assert_equal(newbalance, balance - Decimal("24.9996"))169balance = newbalance171self.log.info("Test transactions conflicted by a double spend")172self.nodes[0].loadwallet("bob")173bob = self.nodes[0].get_wallet_rpc("bob")175# Create a double spend of AB1 by spending again from only A's 10 output176# Mine double spend from node 1177inputs = []178inputs.append({"txid": txA, "vout": nA})179outputs = {}180outputs[self.nodes[1].getnewaddress()] = Decimal("3.9999")181outputs[bob.getnewaddress()] = Decimal("5.9999")182tx = alice.createrawtransaction(inputs, outputs)183signed = alice.signrawtransactionwithwallet(tx)184double_spend_txid = self.nodes[1].sendrawtransaction(signed["hex"])185self.connect_nodes(0, 1)186self.generate(self.nodes[1], 1)188tx_list = alice.listtransactions()190conflicted = [tx for tx in tx_list if tx["confirmations"] < 0]191assert_equal(4, len(conflicted))193wallet_conflicts = [tx for tx in conflicted if tx["walletconflicts"]]194assert_equal(2, len(wallet_conflicts))196double_spends = [tx for tx in tx_list if tx["walletconflicts"] and tx["confirmations"] > 0]197assert_equal(2, len(double_spends)) # one for each output198double_spend = double_spends[0]200# Test the properties of the conflicted transactions, i.e. with confirmations < 0.201for tx in conflicted:202assert_equal(tx["abandoned"], False)203assert_equal(tx["confirmations"], -1)204assert_equal(tx["trusted"], False)206# Test the properties of the double-spend transaction, i.e. having wallet conflicts and confirmations > 0.207assert_equal(double_spend["abandoned"], False)208assert_equal(double_spend["confirmations"], 1)209assert "trusted" not in double_spend.keys() # "trusted" only returned if tx has 0 or negative confirmations.211# Test the walletconflicts field of each.212for tx in wallet_conflicts:213assert_equal(double_spend["walletconflicts"], [tx["txid"]])214assert_equal(tx["walletconflicts"], [double_spend["txid"]])216# Test walletconflicts on the receiver's side217txinfo = bob.gettransaction(txAB1)218assert_equal(txinfo['confirmations'], -1)219assert_equal(txinfo['walletconflicts'], [double_spend['txid']])221double_spends = [tx for tx in bob.listtransactions() if tx["walletconflicts"] and tx["confirmations"] > 0]222assert_equal(1, len(double_spends))223double_spend = double_spends[0]224assert_equal(double_spend_txid, double_spend['txid'])225assert_equal(double_spend["walletconflicts"], [txAB1])227# Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted228assert_equal(alice.gettransaction(txAB1)["confirmations"], -1)229newbalance = alice.getbalance()230assert_equal(newbalance, balance + Decimal("20"))231balance = newbalance233# Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available234blk = self.nodes[0].getbestblockhash()235# mine 10 blocks so that when the blk is invalidated, the transactions are not236# returned to the mempool237self.generate(self.nodes[1], 10)238self.nodes[0].invalidateblock(blk)239assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)240newbalance = alice.getbalance()241assert_equal(newbalance, balance - Decimal("20"))243if __name__ == '__main__':244AbandonConflictTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.