mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 12:02:48 +01:00
6099b4bf39
5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 bitcoind: update -avoidpartialspends description to account for auto-enable for avoid_reuse wallets (Karl-Johan Alm)
ada258f8c8f92d44d893cf9f22d15acdeca40b1a doc: release notes for avoid_reuse (Karl-Johan Alm)
27669551da52099e4a6a401acd7aa32b32832423 wallet: enable avoid_partial_spends by default if avoid_reuse is set (Karl-Johan Alm)
8f2e208f7c0468f9ba92bc789a698281b1c81284 test: add test for avoidreuse feature (Karl-Johan Alm)
0bdfbd34cf4015de87741ff549db35e5064f4e16 wallet/rpc: add 'avoid_reuse' option to RPC commands (Karl-Johan Alm)
f904723e0d5883309cb0dd14b826bc45c5e776fb wallet/rpc: add setwalletflag RPC and MUTABLE_WALLET_FLAGS (Karl-Johan Alm)
8247a0da3a46d7c38943ee0304343ab7465305bd wallet: enable avoid_reuse feature (Karl-Johan Alm)
eec15662fad917b169f5e3b8baaf4301dcf00a7b wallet: avoid reuse flags (Karl-Johan Alm)
58928098c299efdc7c5ddf2dc20716ca5272f21b wallet: make IsWalletFlagSet() const (Karl-Johan Alm)
129a5bafd9a3efa2fa16d780885048a06566d262 wallet: rename g_known_wallet_flags constant to KNOWN_WALLET_FLAGS (Karl-Johan Alm)
Pull request description:
Add a new wallet flag called `avoid_reuse` which, when enabled, will keep track of when a specific destination has been spent from, and will actively "blacklist" any new UTXOs which send to an already-spent-from destination.
This improves privacy, as a payer could otherwise begin tracking a payee's wallet by regularly peppering a known UTXO with dust outputs, which would then be scooped up and used in payments by the payee, allowing the payer to map out (1) the inputs owned by the payee and (2) the destinations to which the payee is making payments.
This replaces #10386 and together with the (now merged) #12257 it addresses #10065 in full. The concerns raised in https://github.com/bitcoin/bitcoin/pull/10386#issuecomment-302361381 are also addressed due to #12257.
~~Note: this builds on top of #15780.~~ (merged)
ACKs for commit 5ebc6b:
jnewbery:
ACK 5ebc6b0eb
laanwj:
Concept and code-review ACK 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80
meshcollider:
Code review ACK 5ebc6b0eb2
achow101:
ACK 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 modulo above nits
Tree-SHA512: fdef45826af544cbbb45634ac367852cc467ec87081d86d08b53ca849e588617e9a0a255b7e7bb28692d15332de58d6c3d274ac003355220e4213d7d9070742e
215 lines
10 KiB
Python
Executable File
215 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2018 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_BCRT1_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 = [
|
|
['-limitdescendantcount=3'], # Limit mempool descendants as a hack to have wallet txs rejected from the mempool
|
|
[],
|
|
]
|
|
|
|
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("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].getbalance(), 500)
|
|
assert_equal(self.nodes[1].getbalance(), 500)
|
|
|
|
self.log.info("Test getbalance with different arguments")
|
|
assert_equal(self.nodes[0].getbalance("*"), 500)
|
|
assert_equal(self.nodes[0].getbalance("*", 1), 500)
|
|
assert_equal(self.nodes[0].getbalance("*", 1, True), 500)
|
|
assert_equal(self.nodes[0].getbalance("*", 1, True, False), 500)
|
|
assert_equal(self.nodes[0].getbalance(minconf=1, addlocked=True), 500)
|
|
assert_equal(self.nodes[0].getbalance(minconf=1, avoid_reuse=False), 500)
|
|
assert_equal(self.nodes[0].getbalance(minconf=1), 500)
|
|
assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 1000)
|
|
assert_equal(self.nodes[1].getbalance(minconf=0, include_watchonly=True), 500)
|
|
|
|
# Send 490 BTC from 0 to 1 and 960 BTC from 1 to 0.
|
|
txs = create_transactions(self.nodes[0], self.nodes[1].getnewaddress(), 490 , [Decimal('0.01')])
|
|
self.nodes[0].sendrawtransaction(txs[0]['hex'])
|
|
self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation
|
|
|
|
self.sync_all()
|
|
txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 960, [Decimal('0.01'), Decimal('0.02')])
|
|
self.nodes[1].sendrawtransaction(txs[0]['hex'])
|
|
self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation
|
|
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")
|
|
|
|
def test_balances(*, fee_node_1=0):
|
|
# getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions
|
|
assert_equal(self.nodes[0].getbalance(), Decimal('9.99')) # change from node 0's send
|
|
assert_equal(self.nodes[1].getbalance(), Decimal('30') - fee_node_1) # change from node 1's send
|
|
# Same with minconf=0
|
|
assert_equal(self.nodes[0].getbalance(minconf=0), Decimal('9.99'))
|
|
assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('30') - fee_node_1)
|
|
# 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
|
|
assert_equal(self.nodes[0].getunconfirmedbalance(), Decimal('960')) # output of node 1's spend
|
|
assert_equal(self.nodes[0].getwalletinfo()["unconfirmed_balance"], Decimal('960'))
|
|
assert_equal(self.nodes[1].getunconfirmedbalance(), Decimal('0')) # Doesn't include output of node 0's send since it was spent
|
|
assert_equal(self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('0'))
|
|
|
|
test_balances(fee_node_1=Decimal('0.01'))
|
|
|
|
# Node 1 bumps the transaction fee and resends
|
|
# self.nodes[1].sendrawtransaction(txs[1]['hex']) # disabled, no RBF in Dash
|
|
#self.nodes[0].sendrawtransaction(txs[1]['hex']) # sending on both nodes is faster than waiting for propagation # disabled, no RBF in Dash
|
|
self.sync_all()
|
|
|
|
self.log.info("Test getbalance and getunconfirmedbalance with conflicted unconfirmed inputs")
|
|
# test_balances(fee_node_1=Decimal('0.02'))
|
|
|
|
self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY)
|
|
self.sync_all()
|
|
|
|
# balances are correct after the transactions are confirmed
|
|
assert_equal(self.nodes[0].getbalance(), Decimal('969.99')) # node 1's send plus change from node 0's send
|
|
assert_equal(self.nodes[1].getbalance(), Decimal('29.99')) # change from node 0's send
|
|
|
|
# Send total balance away from node 1
|
|
txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), Decimal('29.98'), [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'))
|
|
|
|
# check mempool transactions count for wallet unconfirmed balance after
|
|
# dynamically loading the wallet.
|
|
before = self.nodes[1].getunconfirmedbalance()
|
|
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()
|
|
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(), 999)
|
|
|
|
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("<q", 999 * 10**8).hex(),
|
|
struct.pack("<q", 998 * 10**8).hex(),
|
|
)
|
|
tx_replace = self.nodes[0].signrawtransactionwithwallet(tx_replace)['hex']
|
|
# Total balance is given by the sum of outputs of the tx
|
|
total_amount = sum([o['value'] for o in self.nodes[0].decoderawtransaction(tx_replace)['vout']])
|
|
self.sync_all()
|
|
self.nodes[1].sendrawtransaction(hexstring=tx_replace, maxfeerate=0)
|
|
|
|
# Now confirm tx_replace
|
|
block_reorg = self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY)[0]
|
|
self.sync_all()
|
|
assert_equal(self.nodes[0].getbalance(minconf=0), total_amount)
|
|
|
|
self.log.info('Put txs back into mempool of node 1 (not node 0)')
|
|
self.nodes[0].invalidateblock(block_reorg)
|
|
self.nodes[1].invalidateblock(block_reorg)
|
|
self.sync_blocks()
|
|
self.nodes[0].syncwithvalidationinterfacequeue()
|
|
assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted
|
|
self.nodes[0].generatetoaddress(1, ADDRESS_WATCHONLY)
|
|
assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted
|
|
|
|
# Now confirm tx_orig
|
|
self.restart_node(1, ['-persistmempool=0', '-checklevel=0'])
|
|
connect_nodes(self.nodes[0], 1)
|
|
connect_nodes(self.nodes[1], 0)
|
|
sync_blocks(self.nodes)
|
|
self.nodes[1].sendrawtransaction(tx_orig)
|
|
self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY)
|
|
self.sync_all()
|
|
assert_equal(self.nodes[0].getbalance(minconf=0), total_amount + 1) # The reorg recovered our fee of 1 coin
|
|
|
|
|
|
if __name__ == '__main__':
|
|
WalletTest().main()
|