diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index a84acea50..7f6f94017 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -1,226 +1,228 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the abandontransaction RPC. The abandontransaction RPC marks a transaction and all its in-wallet descendants as abandoned which allows their inputs to be respent. It can be used to replace "stuck" or evicted transactions. It only works on transactions which are not included in a block and are not currently in the mempool. It has no effect on transactions which are already abandoned. """ from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, connect_nodes, disconnect_nodes, satoshi_round, ) class AbandonConflictTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-minrelaytxfee=0.00001"], []] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): def total_fees(*txids): total = 0 for txid in txids: # '-=' is because gettransaction(txid)['fee'] returns a negative total -= self.nodes[0].gettransaction(txid)['fee'] return satoshi_round(total) self.nodes[1].generate(100) self.sync_blocks() balance = self.nodes[0].getbalance() txA = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10")) txB = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10")) txC = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10")) self.sync_mempools() self.nodes[1].generate(1) # Can not abandon non-wallet transaction assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: self.nodes[0].abandontransaction(txid='ff' * 32)) # Can not abandon confirmed transaction assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: self.nodes[0].abandontransaction(txid=txA)) self.sync_blocks() newbalance = self.nodes[0].getbalance() # no more than fees lost assert balance - newbalance <= total_fees(txA, txB, txC) balance = newbalance # Disconnect nodes so node0's transactions don't get into node1's # mempool disconnect_nodes(self.nodes[0], self.nodes[1]) # Identify the 10btc outputs nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txA)["details"] if tx_out["amount"] == Decimal("10")) nB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txB)["details"] if tx_out["amount"] == Decimal("10")) nC = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txC)["details"] if tx_out["amount"] == Decimal("10")) inputs = [] # spend 10btc outputs from txA and txB inputs.append({"txid": txA, "vout": nA}) inputs.append({"txid": txB, "vout": nB}) outputs = {} outputs[self.nodes[0].getnewaddress()] = Decimal("14.99998") outputs[self.nodes[1].getnewaddress()] = Decimal("5") signed = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) # Identify the 14.99998btc output nAB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txAB1)["details"] if tx_out["amount"] == Decimal("14.99998")) # Create a child tx spending AB1 and C inputs = [] # Amount 14.99998 BCH inputs.append({"txid": txAB1, "vout": nAB}) # Amount 10 BCH inputs.append({"txid": txC, "vout": nC}) outputs = {} outputs[self.nodes[0].getnewaddress()] = Decimal("24.9996") signed2 = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) # Create a child tx spending ABC2 signed3_change = Decimal("24.999") inputs = [{"txid": txABC2, "vout": 0}] outputs = {self.nodes[0].getnewaddress(): signed3_change} signed3 = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs)) # note tx is never directly referenced, only abandoned as a child of # the above self.nodes[0].sendrawtransaction(signed3["hex"]) # In mempool txs from self should increase balance from change newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("30") + signed3_change) balance = newbalance # Restart the node with a higher min relay fee so the parent tx is no longer in mempool # TODO: redo with eviction self.stop_node(0) self.start_node(0, extra_args=["-minrelaytxfee=0.0001"]) assert self.nodes[0].getmempoolinfo()['loaded'] # Verify txs no longer in either node's mempool assert_equal(len(self.nodes[0].getrawmempool()), 0) assert_equal(len(self.nodes[1].getrawmempool()), 0) # Transactions which are not in the mempool should only reduce wallet balance. # Transaction inputs should still be spent, but the change not yet # received. newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - signed3_change) # Unconfirmed received funds that are not in mempool also shouldn't show # up in unconfirmed balance. Note that the transactions stored in the wallet # are not necessarily in the node's mempool. - unconfbalance = self.nodes[0].getunconfirmedbalance( - ) + self.nodes[0].getbalance() - assert_equal(unconfbalance, newbalance) + balances = self.nodes[0].getbalances()['mine'] + assert_equal( + balances['untrusted_pending'] + + balances['trusted'], + newbalance) # Unconfirmed transactions which are not in the mempool should also # not be in listunspent assert txABC2 not in [utxo["txid"] for utxo in self.nodes[0].listunspent(0)] balance = newbalance # Abandon original transaction and verify inputs are available again # including that the child tx was also abandoned self.nodes[0].abandontransaction(txAB1) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance + Decimal("30")) balance = newbalance # Verify that even with a low min relay fee, the tx is not re-accepted # from wallet on startup once abandoned. self.stop_node(0) self.start_node(0, extra_args=["-minrelaytxfee=0.00001"]) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) assert_equal(self.nodes[0].getbalance(), balance) # If the transaction is re-sent the wallet also unabandons it. The # change should be available, and it's child transaction should remain # abandoned. # NOTE: Abandoned transactions are internal to the wallet, and tracked # separately from other indices. self.nodes[0].sendrawtransaction(signed["hex"]) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("20") + Decimal("14.99998")) balance = newbalance # Send child tx again so it is no longer abandoned. self.nodes[0].sendrawtransaction(signed2["hex"]) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("10") - Decimal("14.99998") + Decimal("24.9996")) balance = newbalance # Reset to a higher relay fee so that we abandon a transaction self.stop_node(0) self.start_node(0, extra_args=["-minrelaytxfee=0.0001"]) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance - Decimal("24.9996")) balance = newbalance # Create a double spend of AB1. Spend it again from only A's 10 output. # Mine double spend from node 1. inputs = [] inputs.append({"txid": txA, "vout": nA}) outputs = {} outputs[self.nodes[1].getnewaddress()] = Decimal("9.9999") tx = self.nodes[0].createrawtransaction(inputs, outputs) signed = self.nodes[0].signrawtransactionwithwallet(tx) self.nodes[1].sendrawtransaction(signed["hex"]) self.nodes[1].generate(1) connect_nodes(self.nodes[0], self.nodes[1]) self.sync_blocks() # Verify that B and C's 10 BCH outputs are available for spending again # because AB1 is now conflicted newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance + Decimal("20")) balance = newbalance # There is currently a minor bug around this and so this test doesn't work. See Issue #7315 # Invalidate the block with the double spend and B's 10 BCH output should no longer be available # Don't think C's should either self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) newbalance = self.nodes[0].getbalance() # assert_equal(newbalance, balance - Decimal("10")) self.log.info( "If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer") self.log.info( "conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315") self.log.info(str(balance) + " -> " + str(newbalance) + " ?") if __name__ == '__main__': AbandonConflictTest().main() diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index c9eecc346..fd4fdd70a 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -1,306 +1,303 @@ #!/usr/bin/env python3 # Copyright (c) 2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the wallet can send and receive using all combinations of address types. There are 4 nodes-under-test: - node0 uses legacy addresses - node1 uses legacy addresses - node2 uses legacy addresses - node3 uses legacy addresses node4 exists to generate new blocks. ## Multisig address test Test that adding a multisig address with: - an uncompressed pubkey always gives a legacy address - only compressed pubkeys gives the an `-addresstype` address ## Sending to address types test A series of tests, iterating over node0-node3. In each iteration of the test, one node sends: - 10/101th of its balance to itself (using getrawchangeaddress for single key addresses) - 20/101th to the next node - 30/101th to the node after that - 40/101th to the remaining node - 1/101th remains as fee+change Iterate over each node for single key addresses, and then over each node for multisig addresses. Repeat test. As every node sends coins after receiving, this also verifies that spending coins sent to all these address types works. """ from decimal import Decimal import itertools from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import ( descsum_create, descsum_check, ) from test_framework.util import ( assert_equal, assert_greater_than, connect_nodes, ) class AddressTypeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 5 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): self.setup_nodes() # Fully mesh-connect nodes for faster mempool sync for i, j in itertools.product(range(self.num_nodes), repeat=2): if i > j: connect_nodes(self.nodes[i], self.nodes[j]) self.sync_all() - def get_balances(self, confirmed=True): - """Return a list of confirmed or unconfirmed balances.""" - if confirmed: - return [self.nodes[i].getbalance() for i in range(4)] - else: - return [self.nodes[i].getunconfirmedbalance() for i in range(4)] + def get_balances(self, key='trusted'): + """Return a list of balances.""" + return [self.nodes[i].getbalances()['mine'][key] for i in range(4)] def test_address(self, node, address, multisig, typ): """Run sanity checks on an address.""" self.log.info(address) info = self.nodes[node].getaddressinfo(address) assert(self.nodes[node].validateaddress(address)['isvalid']) assert_equal(info.get('solvable'), True) if not multisig and typ == 'legacy': # P2PKH assert(not info['isscript']) assert('pubkey' in info) elif typ == 'legacy': # P2SH-multisig assert(info['isscript']) assert_equal(info['script'], 'multisig') assert('pubkeys' in info) else: # Unknown type assert(False) def test_desc(self, node, address, multisig, typ, utxo): """Run sanity checks on a descriptor reported by getaddressinfo.""" info = self.nodes[node].getaddressinfo(address) assert('desc' in info) assert_equal(info['desc'], utxo['desc']) assert(self.nodes[node].validateaddress(address)['isvalid']) # Use a ridiculously roundabout way to find the key origin info through # the PSBT logic. However, this does test consistency between the PSBT reported # fingerprints/paths and the descriptor logic. psbt = self.nodes[node].createpsbt( [{'txid': utxo['txid'], 'vout':utxo['vout']}], [{address: 0.00010000}]) psbt = self.nodes[node].walletprocesspsbt( psbt, False, "ALL|FORKID", True) decode = self.nodes[node].decodepsbt(psbt['psbt']) key_descs = {} for deriv in decode['inputs'][0]['bip32_derivs']: assert_equal(len(deriv['master_fingerprint']), 8) assert_equal(deriv['path'][0], 'm') key_descs[deriv['pubkey']] = '[' + deriv['master_fingerprint'] + \ deriv['path'][1:] + ']' + deriv['pubkey'] # Verify the descriptor checksum against the Python implementation assert(descsum_check(info['desc'])) # Verify that stripping the checksum and recreating it using Python # roundtrips assert(info['desc'] == descsum_create(info['desc'][:-9])) # Verify that stripping the checksum and feeding it to # getdescriptorinfo roundtrips assert(info['desc'] == self.nodes[0].getdescriptorinfo( info['desc'][:-9])['descriptor']) assert_equal( info['desc'][-8:], self.nodes[0].getdescriptorinfo(info['desc'][:-9])['checksum']) # Verify that keeping the checksum and feeding it to getdescriptorinfo # roundtrips assert info['desc'] == self.nodes[0].getdescriptorinfo(info['desc'])[ 'descriptor'] assert_equal(info['desc'][-8:], self.nodes[0].getdescriptorinfo(info['desc'])['checksum']) if not multisig and typ == 'legacy': # P2PKH assert_equal(info['desc'], descsum_create("pkh({})".format(key_descs[info['pubkey']]))) elif typ == 'legacy': # P2SH-multisig assert_equal(info['desc'], descsum_create("sh(multi(2,{},{}))".format( key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))) else: # Unknown type assert(False) def test_change_output_type( self, node_sender, destinations, expected_type): txid = self.nodes[node_sender].sendmany( dummy="", amounts=dict.fromkeys( destinations, 0.001)) raw_tx = self.nodes[node_sender].getrawtransaction(txid) tx = self.nodes[node_sender].decoderawtransaction(raw_tx) # Make sure the transaction has change: assert_equal(len(tx["vout"]), len(destinations) + 1) # Make sure the destinations are included, and remove them: output_addresses = [vout['scriptPubKey']['addresses'][0] for vout in tx["vout"]] change_addresses = [ d for d in output_addresses if d not in destinations] assert_equal(len(change_addresses), 1) self.log.debug( "Check if change address " + change_addresses[0] + " is " + expected_type) self.test_address( node_sender, change_addresses[0], multisig=False, typ=expected_type) def run_test(self): # Mine 101 blocks on node4 to bring nodes out of IBD and make sure that # no coinbases are maturing for the nodes-under-test during the test self.nodes[4].generate(101) self.sync_blocks() uncompressed_1 = "0496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee" uncompressed_2 = "047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77" compressed_1 = "0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52" compressed_2 = "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073" # addmultisigaddress with at least 1 uncompressed key should return a # legacy address. for node in range(4): self.test_address(node, self.nodes[node].addmultisigaddress( 2, [uncompressed_1, uncompressed_2])['address'], True, 'legacy') self.test_address(node, self.nodes[node].addmultisigaddress( 2, [compressed_1, uncompressed_2])['address'], True, 'legacy') self.test_address(node, self.nodes[node].addmultisigaddress( 2, [uncompressed_1, compressed_2])['address'], True, 'legacy') # addmultisigaddress with all compressed keys should return the # appropriate address type (even when the keys are not ours). self.test_address(0, self.nodes[0].addmultisigaddress( 2, [compressed_1, compressed_2])['address'], True, 'legacy') for multisig, from_node in itertools.product([False, True], range(4)): self.log.info( "Sending from node {} with{} multisig".format(from_node, "" if multisig else "out")) old_balances = self.get_balances() self.log.debug("Old balances are {}".format(old_balances)) to_send = ( old_balances[from_node] / 101).quantize( Decimal("0.00000001")) sends = {} addresses = {} self.log.debug("Prepare sends") for n, to_node in enumerate(range(from_node, from_node + 4)): to_node %= 4 if not multisig: if from_node == to_node: # When sending non-multisig to self, use # getrawchangeaddress address = self.nodes[to_node].getrawchangeaddress() else: address = self.nodes[to_node].getnewaddress() else: addr1 = self.nodes[to_node].getnewaddress() addr2 = self.nodes[to_node].getnewaddress() address = self.nodes[to_node].addmultisigaddress(2, [addr1, addr2])[ 'address'] # Do some sanity checking on the created address typ = 'legacy' self.test_address(to_node, address, multisig, typ) # Output entry sends[address] = to_send * 10 * (1 + n) addresses[to_node] = (address, typ) self.log.debug("Sending: {}".format(sends)) self.nodes[from_node].sendmany("", sends) self.sync_mempools() - unconf_balances = self.get_balances(False) + unconf_balances = self.get_balances('untrusted_pending') self.log.debug( "Check unconfirmed balances: {}".format(unconf_balances)) assert_equal(unconf_balances[from_node], 0) for n, to_node in enumerate(range(from_node + 1, from_node + 4)): to_node %= 4 assert_equal(unconf_balances[to_node], to_send * 10 * (2 + n)) # node4 collects fee and block subsidy to keep accounting simple self.nodes[4].generate(1) self.sync_blocks() # Verify that the receiving wallet contains a UTXO with the # expected address, and expected descriptor for n, to_node in enumerate(range(from_node, from_node + 4)): to_node %= 4 found = False for utxo in self.nodes[to_node].listunspent(): if utxo['address'] == addresses[to_node][0]: found = True self.test_desc( to_node, addresses[to_node][0], multisig, addresses[to_node][1], utxo) break assert found new_balances = self.get_balances() self.log.debug("Check new balances: {}".format(new_balances)) # We don't know what fee was set, so we can only check bounds on # the balance of the sending node assert_greater_than(new_balances[from_node], to_send * 10) assert_greater_than(to_send * 11, new_balances[from_node]) for n, to_node in enumerate(range(from_node + 1, from_node + 4)): to_node %= 4 assert_equal( new_balances[to_node], old_balances[to_node] + to_send * 10 * (2 + n)) # Get addresses from node2 and node3: to_address_2 = self.nodes[2].getnewaddress() to_address_3_1 = self.nodes[3].getnewaddress() to_address_3_2 = self.nodes[3].getnewaddress() self.log.info("Various change output tests") self.test_change_output_type(0, [to_address_3_1], 'legacy') self.test_change_output_type(1, [to_address_2], 'legacy') self.test_change_output_type(1, [to_address_3_1], 'legacy') self.test_change_output_type( 1, [to_address_2, to_address_3_1], 'legacy') self.test_change_output_type( 1, [to_address_3_1, to_address_3_2], 'legacy') self.test_change_output_type(2, [to_address_3_1], 'legacy') self.log.info('Test getrawchangeaddress') self.test_address( 3, self.nodes[3].getrawchangeaddress(), multisig=False, typ='legacy') if __name__ == '__main__': AddressTypeTest().main() diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index a296258e6..ed436fcfb 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -1,300 +1,317 @@ #!/usr/bin/env python3 # Copyright (c) 2018-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet balance RPC methods.""" from decimal import Decimal import struct from test_framework.address import ADDRESS_BCHREG_UNSPENDABLE as ADDRESS_WATCHONLY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, connect_nodes, sync_blocks, ) def create_transactions(node, address, amt, fees): # Create and sign raw transactions from node to address for amt. # Creates a transaction for each fee and returns an array # of the raw transactions. utxos = [u for u in node.listunspent(0) if u['spendable']] # Create transactions inputs = [] ins_total = 0 for utxo in utxos: inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]}) ins_total += utxo['amount'] if ins_total >= amt + max(fees): break # make sure there was enough utxos assert ins_total >= amt + max(fees) txs = [] for fee in fees: outputs = {address: amt} # prevent 0 change output if ins_total > amt + fee: outputs[node.getrawchangeaddress()] = ins_total - amt - fee raw_tx = node.createrawtransaction(inputs, outputs, 0) raw_tx = node.signrawtransactionwithwallet(raw_tx) assert_equal(raw_tx['complete'], True) txs.append(raw_tx) return txs class WalletTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ # Limit mempool descendants as a hack to have wallet txs rejected # from the mempool ['-limitdescendantcount=3'], [], ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): self.nodes[0].importaddress(ADDRESS_WATCHONLY) # Check that nodes don't own any UTXOs assert_equal(len(self.nodes[0].listunspent()), 0) assert_equal(len(self.nodes[1].listunspent()), 0) self.log.info("Check that only node 0 is watching an address") assert 'watchonly' in self.nodes[0].getbalances() assert 'watchonly' not in self.nodes[1].getbalances() self.log.info("Mining blocks ...") self.nodes[0].generate(1) self.sync_all() self.nodes[1].generate(1) self.nodes[1].generatetoaddress(101, ADDRESS_WATCHONLY) self.sync_all() assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50) assert_equal(self.nodes[0].getwalletinfo()['balance'], 50) assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50) assert_equal(self.nodes[0].getbalances()[ 'watchonly']['immature'], 5000) assert 'watchonly' not in self.nodes[1].getbalances() assert_equal(self.nodes[0].getbalance(), 50) assert_equal(self.nodes[1].getbalance(), 50) self.log.info("Test getbalance with different arguments") assert_equal(self.nodes[0].getbalance("*"), 50) assert_equal(self.nodes[0].getbalance("*", 1), 50) assert_equal(self.nodes[0].getbalance("*", 1, True), 100) assert_equal(self.nodes[0].getbalance(minconf=1), 50) assert_equal( self.nodes[0].getbalance( minconf=0, include_watchonly=True), 100) assert_equal( self.nodes[1].getbalance( minconf=0, include_watchonly=True), 50) # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. txs = create_transactions( self.nodes[0], self.nodes[1].getnewaddress(), 40, [Decimal('0.01')]) self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[1].sendrawtransaction(txs[0]['hex']) self.sync_all() txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60, [ Decimal('0.01'), Decimal('0.02')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[0].sendrawtransaction(txs[0]['hex']) self.sync_all() # First argument of getbalance must be set to "*" assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "") - self.log.info( - "Test getbalance and getunconfirmedbalance with unconfirmed inputs") + self.log.info("Test balances with unconfirmed inputs") # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to #16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After #16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after #16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post #16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. def test_balances(*, fee_node_1=0): + # getbalances + expected_balances_0 = {'mine': {'immature': Decimal('0E-8'), + # change from node 0's send + 'trusted': Decimal('9.99'), + 'untrusted_pending': Decimal('60.0')}, + 'watchonly': {'immature': Decimal('5000'), + 'trusted': Decimal('50.0'), + 'untrusted_pending': Decimal('0E-8')}} + expected_balances_1 = {'mine': {'immature': Decimal('0E-8'), + # node 1's send had an unsafe input + 'trusted': Decimal('0E-8'), + # Doesn't include output of node + # 0's send since it was spent + 'untrusted_pending': Decimal('30.0') - fee_node_1}} + assert_equal(self.nodes[0].getbalances(), expected_balances_0) + assert_equal(self.nodes[1].getbalances(), expected_balances_1) # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions # change from node 0's send assert_equal(self.nodes[0].getbalance(), Decimal('9.99')) # node 1's send had an unsafe input assert_equal(self.nodes[1].getbalance(), Decimal('0')) # Same with minconf=0 assert_equal(self.nodes[0].getbalance(minconf=0), Decimal('9.99')) assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('0')) # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth assert_equal(self.nodes[0].getbalance(minconf=1), Decimal('0')) assert_equal(self.nodes[1].getbalance(minconf=1), Decimal('0')) # getunconfirmedbalance # output of node 1's spend assert_equal(self.nodes[0].getunconfirmedbalance(), Decimal('60')) - assert_equal(self.nodes[0].getbalances()['mine'] - ['untrusted_pending'], Decimal('60')) - assert_equal(self.nodes[0].getwalletinfo()[ - "unconfirmed_balance"], Decimal('60')) # Doesn't include output of node 0's send since it was spent assert_equal( self.nodes[1].getunconfirmedbalance(), Decimal('30') - fee_node_1) - assert_equal( - self.nodes[1].getbalances()['mine']['untrusted_pending'], - Decimal('30') - fee_node_1) + # getwalletinfo.unconfirmed_balance + assert_equal(self.nodes[0].getwalletinfo()[ + "unconfirmed_balance"], Decimal('60')) assert_equal( self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('30') - fee_node_1) test_balances(fee_node_1=Decimal('0.01')) # In the original Core version of this test, Node 1 would've bumped # the fee by 0.01 here to resend, but this is BCH, so it has 0.01 BCH # left to spend on goods and services self.sync_all() self.log.info( - "Test getbalance and getunconfirmedbalance with conflicted unconfirmed inputs") + "Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs") test_balances(fee_node_1=Decimal('0.01')) self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY) self.sync_all() # balances are correct after the transactions are confirmed # node 1's send plus change from node 0's send - assert_equal(self.nodes[0].getbalance(), Decimal('69.99')) - assert_equal(self.nodes[1].getbalance(), Decimal( - '29.99')) # change from node 0's send + balance_node0 = Decimal('69.99') + # change from node 0's send + balance_node1 = Decimal('29.99') + assert_equal(self.nodes[0].getbalances()[ + 'mine']['trusted'], balance_node0) + assert_equal(self.nodes[1].getbalances()[ + 'mine']['trusted'], balance_node1) + assert_equal(self.nodes[0].getbalance(), balance_node0) + assert_equal(self.nodes[1].getbalance(), balance_node1) # Send total balance away from node 1 txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress( ), Decimal('29.97'), [Decimal('0.01')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) self.nodes[1].generatetoaddress(2, ADDRESS_WATCHONLY) self.sync_all() # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth # getbalance with minconf=3 should still show the old balance assert_equal(self.nodes[1].getbalance(minconf=3), Decimal('0')) # getbalance with minconf=2 will show the new balance. assert_equal(self.nodes[1].getbalance(minconf=2), Decimal('0.01')) # check mempool transactions count for wallet unconfirmed balance after # dynamically loading the wallet. - before = self.nodes[1].getunconfirmedbalance() + before = self.nodes[1].getbalances()['mine']['untrusted_pending'] dst = self.nodes[1].getnewaddress() self.nodes[1].unloadwallet('') self.nodes[0].sendtoaddress(dst, 0.1) self.sync_all() self.nodes[1].loadwallet('') - after = self.nodes[1].getunconfirmedbalance() + after = self.nodes[1].getbalances()['mine']['untrusted_pending'] assert_equal(before + Decimal('0.1'), after) # Create 3 more wallet txs, where the last is not accepted to the # mempool because it is the third descendant of the tx above for _ in range(3): # Set amount high enough such that all coins are spent by each tx txid = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), 99) self.log.info('Check that wallet txs not in the mempool are untrusted') assert txid not in self.nodes[0].getrawmempool() assert_equal(self.nodes[0].gettransaction(txid)['trusted'], False) assert_equal(self.nodes[0].getbalance(minconf=0), 0) self.log.info("Test replacement and reorg of non-mempool tx") tx_orig = self.nodes[0].gettransaction(txid)['hex'] # Increase fee by 1 coin tx_replace = tx_orig.replace( struct.pack("