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) 2018-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 wallet balance RPC methods."""6from decimal import Decimal7import time9from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY10from test_framework.blocktools import COINBASE_MATURITY11from test_framework.descriptors import descsum_create12from test_framework.test_framework import BitcoinTestFramework13from test_framework.util import (14assert_equal,15assert_is_hash_string,16assert_raises_rpc_error,17)18from test_framework.wallet_util import get_generate_key21def create_transactions(node, address, amt, fees):22# Create and sign raw transactions from node to address for amt.23# Creates a transaction for each fee and returns an array24# of the raw transactions.25utxos = [u for u in node.listunspent(0) if u['spendable']]27# Create transactions28inputs = []29ins_total = 030for utxo in utxos:31inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]})32ins_total += utxo['amount']33if ins_total >= amt + max(fees):34break35# make sure there was enough utxos36assert ins_total >= amt + max(fees)38txs = []39for fee in fees:40outputs = {address: amt}41# prevent 0 change output42if ins_total > amt + fee:43outputs[node.getrawchangeaddress()] = ins_total - amt - fee44raw_tx = node.createrawtransaction(inputs, outputs, 0, True)45raw_tx = node.signrawtransactionwithwallet(raw_tx)46assert_equal(raw_tx['complete'], True)47txs.append(raw_tx)49return txs51class WalletTest(BitcoinTestFramework):52def set_test_params(self):53self.num_nodes = 254self.setup_clean_chain = True55# whitelist peers to speed up tx relay / mempool sync56self.noban_tx_relay = True57self.extra_args = [58# Limit mempool clusters as a hack to have wallet txs rejected from the mempool.59# Set walletrejectlongchains=0 so the wallet still creates the transactions.60['-limitclustercount=3', '-walletrejectlongchains=0'],61[],62]64def skip_test_if_missing_module(self):65self.skip_if_no_wallet()67def run_test(self):68self.log.info("Mining blocks ...")69self.generate(self.nodes[0], 1)70self.generate(self.nodes[1], 1)72# Verify listunspent returns immature coinbase if 'include_immature_coinbase' is set73assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1)74assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 0)76self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, ADDRESS_WATCHONLY)78# Verify listunspent returns all immature coinbases if 'include_immature_coinbase' is set79assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 1)80assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1)82self.log.info("Test getbalance with different arguments")83assert_equal(self.nodes[0].getbalance("*"), 50)84assert_equal(self.nodes[0].getbalance("*", 1), 50)85assert_equal(self.nodes[0].getbalance(minconf=1), 50)86assert_equal(self.nodes[0].getbalance(minconf=0), 50)87assert_equal(self.nodes[0].getbalance("*", 1, True), 50)88assert_equal(self.nodes[1].getbalance(minconf=0), 50)90# Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0.91txs = create_transactions(self.nodes[0], self.nodes[1].getnewaddress(), 40, [Decimal('0.01')])92self.nodes[0].sendrawtransaction(txs[0]['hex'])93self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation95self.sync_all()96txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60, [Decimal('0.01'), Decimal('0.02')])97self.nodes[1].sendrawtransaction(txs[0]['hex'])98self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation99self.sync_all()101# First argument of getbalance must be set to "*"102assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "")104self.log.info("Test balances with unconfirmed inputs")106# Before `test_balance()`, we have had two nodes with a balance of 50107# each and then we:108#109# 1) Sent 40 from node A to node B with fee 0.01110# 2) Sent 60 from node B to node A with fee 0.01111#112# Then we check the balances:113#114# 1) As is115# 2) With transaction 2 from above with 2x the fee116#117# Prior to #16766, in this situation, the node would immediately report118# a balance of 30 on node B as unconfirmed and trusted.119#120# After #16766, we show that balance as unconfirmed.121#122# The balance is indeed "trusted" and "confirmed" insofar as removing123# the mempool transactions would return at least that much money. But124# the algorithm after #16766 marks it as unconfirmed because the 'taint'125# tracking of transaction trust for summing balances doesn't consider126# which inputs belong to a user. In this case, the change output in127# question could be "destroyed" by replace the 1st transaction above.128#129# The post #16766 behavior is correct; we shouldn't be treating those130# funds as confirmed. If you want to rely on that specific UTXO existing131# which has given you that balance, you cannot, as a third party132# spending the other input would destroy that unconfirmed.133#134# For example, if the test transactions were:135#136# 1) Sent 40 from node A to node B with fee 0.01137# 2) Sent 10 from node B to node A with fee 0.01138#139# Then our node would report a confirmed balance of 40 + 50 - 10 = 80140# BTC, which is more than would be available if transaction 1 were141# replaced.144def test_balances(*, fee_node_1=0):145# getbalances146expected_balances_0 = {'mine': {'immature': Decimal('0E-8'),147'trusted': Decimal('9.99'), # change from node 0's send148'untrusted_pending': Decimal('60.0'),149'nonmempool': Decimal('0.0')}}150expected_balances_1 = {'mine': {'immature': Decimal('0E-8'),151'trusted': Decimal('0E-8'), # node 1's send had an unsafe input152'untrusted_pending': Decimal('30.0') - fee_node_1, # Doesn't include output of node 0's send since it was spent153'nonmempool': Decimal('0.0')}}154balances_0 = self.nodes[0].getbalances()155balances_1 = self.nodes[1].getbalances()156# remove lastprocessedblock keys (they will be tested later)157del balances_0['lastprocessedblock']158del balances_1['lastprocessedblock']159assert_equal(balances_0, expected_balances_0)160assert_equal(balances_1, expected_balances_1)161# getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions162assert_equal(self.nodes[0].getbalance(), Decimal('9.99')) # change from node 0's send163assert_equal(self.nodes[1].getbalance(), Decimal('0')) # node 1's send had an unsafe input164# Same with minconf=0165assert_equal(self.nodes[0].getbalance(minconf=0), Decimal('9.99'))166assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('0'))167# getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago168# TODO: fix getbalance tracking of coin spentness depth169assert_equal(self.nodes[0].getbalance(minconf=1), Decimal('0'))170assert_equal(self.nodes[1].getbalance(minconf=1), Decimal('0'))172test_balances(fee_node_1=Decimal('0.01'))174# Node 1 bumps the transaction fee and resends175self.nodes[1].sendrawtransaction(txs[1]['hex'])176self.nodes[0].sendrawtransaction(txs[1]['hex']) # sending on both nodes is faster than waiting for propagation177self.sync_all()179self.log.info("Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs")180test_balances(fee_node_1=Decimal('0.02'))182self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)184# balances are correct after the transactions are confirmed185balance_node0 = Decimal('69.99') # node 1's send plus change from node 0's send186balance_node1 = Decimal('29.98') # change from node 0's send187assert_equal(self.nodes[0].getbalances()['mine']['trusted'], balance_node0)188assert_equal(self.nodes[1].getbalances()['mine']['trusted'], balance_node1)189assert_equal(self.nodes[0].getbalance(), balance_node0)190assert_equal(self.nodes[1].getbalance(), balance_node1)192# Send total balance away from node 1193txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), Decimal('29.97'), [Decimal('0.01')])194self.nodes[1].sendrawtransaction(txs[0]['hex'])195self.generatetoaddress(self.nodes[1], 2, ADDRESS_WATCHONLY)197# getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago198# TODO: fix getbalance tracking of coin spentness depth199# getbalance with minconf=3 should still show the old balance200assert_equal(self.nodes[1].getbalance(minconf=3), Decimal('0'))202# getbalance with minconf=2 will show the new balance.203assert_equal(self.nodes[1].getbalance(minconf=2), Decimal('0'))205# check mempool transactions count for wallet unconfirmed balance after206# dynamically loading the wallet.207before = self.nodes[1].getbalances()['mine']['untrusted_pending']208dst = self.nodes[1].getnewaddress()209self.nodes[1].unloadwallet(self.default_wallet_name)210self.nodes[0].sendtoaddress(dst, 0.1)211self.sync_all()212self.nodes[1].loadwallet(self.default_wallet_name)213after = self.nodes[1].getbalances()['mine']['untrusted_pending']214assert_equal(before + Decimal('0.1'), after)216# Create 3 more wallet txs, where the last is not accepted to the217# mempool because it is the third descendant of the tx above218for _ in range(3):219# Set amount high enough such that all coins are spent by each tx220txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 99)222self.log.info('Check that wallet txs not in the mempool are untrusted')223assert txid not in self.nodes[0].getrawmempool()224assert_equal(self.nodes[0].gettransaction(txid)['trusted'], False)225assert_equal(self.nodes[0].getbalance(minconf=0), 0)227self.log.info("Test replacement and reorg of non-mempool tx")228tx_orig = self.nodes[0].gettransaction(txid)['hex']229# Increase fee by 1 coin230tx_replace = tx_orig.replace(231(99 * 10**8).to_bytes(8, "little", signed=True).hex(),232(98 * 10**8).to_bytes(8, "little", signed=True).hex(),233)234tx_replace = self.nodes[0].signrawtransactionwithwallet(tx_replace)['hex']235# Total balance is given by the sum of outputs of the tx236total_amount = sum([o['value'] for o in self.nodes[0].decoderawtransaction(tx_replace)['vout']])237self.sync_all()238self.nodes[1].sendrawtransaction(hexstring=tx_replace, maxfeerate=0)240# Now confirm tx_replace241block_reorg = self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)[0]242assert_equal(self.nodes[0].getbalance(minconf=0), total_amount)244self.log.info('Put txs back into mempool of node 1 (not node 0)')245self.nodes[0].invalidateblock(block_reorg)246self.nodes[1].invalidateblock(block_reorg)247assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted248self.generatetoaddress(self.nodes[0], 1, ADDRESS_WATCHONLY, sync_fun=self.no_op)250# Now confirm tx_orig
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.