|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2018 The Bitcoin Core 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 avoid_reuse and setwalletflag features.""" |
| 6 | + |
| 7 | +from test_framework.test_framework import BitcoinTestFramework |
| 8 | +from test_framework.util import ( |
| 9 | + assert_equal, |
| 10 | + assert_raises_rpc_error, |
| 11 | + connect_nodes_bi, |
| 12 | +) |
| 13 | + |
| 14 | +# TODO: Copied from wallet_groups.py -- should perhaps move into util.py |
| 15 | +def assert_approx(v, vexp, vspan=0.00001): |
| 16 | + if v < vexp - vspan: |
| 17 | + raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) |
| 18 | + if v > vexp + vspan: |
| 19 | + raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) |
| 20 | + |
| 21 | +def reset_balance(node, discardaddr): |
| 22 | + '''Throw away all owned coins by the node so it gets a balance of 0.''' |
| 23 | + balance = node.getbalance(avoid_reuse=False) |
| 24 | + if balance > 0.5: |
| 25 | + node.sendtoaddress(address=discardaddr, amount=balance, subtractfeefromamount=True, avoid_reuse=False) |
| 26 | + |
| 27 | +def count_unspent(node): |
| 28 | + '''Count the unspent outputs for the given node and return various statistics''' |
| 29 | + r = { |
| 30 | + "total": { |
| 31 | + "count": 0, |
| 32 | + "sum": 0, |
| 33 | + }, |
| 34 | + "reused": { |
| 35 | + "count": 0, |
| 36 | + "sum": 0, |
| 37 | + }, |
| 38 | + } |
| 39 | + supports_reused = True |
| 40 | + for utxo in node.listunspent(minconf=0): |
| 41 | + r["total"]["count"] += 1 |
| 42 | + r["total"]["sum"] += utxo["amount"] |
| 43 | + if supports_reused and "reused" in utxo: |
| 44 | + if utxo["reused"]: |
| 45 | + r["reused"]["count"] += 1 |
| 46 | + r["reused"]["sum"] += utxo["amount"] |
| 47 | + else: |
| 48 | + supports_reused = False |
| 49 | + r["reused"]["supported"] = supports_reused |
| 50 | + return r |
| 51 | + |
| 52 | +def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None): |
| 53 | + '''Make assertions about a node's unspent output statistics''' |
| 54 | + stats = count_unspent(node) |
| 55 | + if total_count is not None: |
| 56 | + assert_equal(stats["total"]["count"], total_count) |
| 57 | + if total_sum is not None: |
| 58 | + assert_approx(stats["total"]["sum"], total_sum, 0.001) |
| 59 | + if reused_supported is not None: |
| 60 | + assert_equal(stats["reused"]["supported"], reused_supported) |
| 61 | + if reused_count is not None: |
| 62 | + assert_equal(stats["reused"]["count"], reused_count) |
| 63 | + if reused_sum is not None: |
| 64 | + assert_approx(stats["reused"]["sum"], reused_sum, 0.001) |
| 65 | + |
| 66 | +class AvoidReuseTest(BitcoinTestFramework): |
| 67 | + |
| 68 | + def set_test_params(self): |
| 69 | + self.setup_clean_chain = False |
| 70 | + self.num_nodes = 2 |
| 71 | + |
| 72 | + def skip_test_if_missing_module(self): |
| 73 | + self.skip_if_no_wallet() |
| 74 | + |
| 75 | + def run_test(self): |
| 76 | + '''Set up initial chain and run tests defined below''' |
| 77 | + |
| 78 | + self.test_persistence() |
| 79 | + self.test_immutable() |
| 80 | + |
| 81 | + self.nodes[0].generate(110) |
| 82 | + self.sync_all() |
| 83 | + reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) |
| 84 | + self.test_fund_send_fund_senddirty() |
| 85 | + reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) |
| 86 | + self.test_fund_send_fund_send() |
| 87 | + |
| 88 | + def test_persistence(self): |
| 89 | + '''Test that wallet files persist the avoid_reuse flag.''' |
| 90 | + # Configure node 1 to use avoid_reuse |
| 91 | + self.nodes[1].setwalletflag('avoid_reuse') |
| 92 | + |
| 93 | + # Flags should be node1.avoid_reuse=false, node2.avoid_reuse=true |
| 94 | + assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) |
| 95 | + assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) |
| 96 | + |
| 97 | + # Stop and restart node 1 |
| 98 | + self.stop_node(1) |
| 99 | + self.start_node(1) |
| 100 | + connect_nodes_bi(self.nodes, 0, 1) |
| 101 | + |
| 102 | + # Flags should still be node1.avoid_reuse=false, node2.avoid_reuse=true |
| 103 | + assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) |
| 104 | + assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) |
| 105 | + |
| 106 | + # Attempting to set flag to its current state should throw |
| 107 | + assert_raises_rpc_error(-8, "Wallet flag is already set to false", self.nodes[0].setwalletflag, 'avoid_reuse', False) |
| 108 | + assert_raises_rpc_error(-8, "Wallet flag is already set to true", self.nodes[1].setwalletflag, 'avoid_reuse', True) |
| 109 | + |
| 110 | + def test_immutable(self): |
| 111 | + '''Test immutable wallet flags''' |
| 112 | + # Attempt to set the disable_private_keys flag; this should not work |
| 113 | + assert_raises_rpc_error(-8, "Wallet flag is immutable", self.nodes[1].setwalletflag, 'disable_private_keys') |
| 114 | + |
| 115 | + tempwallet = ".wallet_avoidreuse.py_test_immutable_wallet.dat" |
| 116 | + |
| 117 | + # Create a wallet with disable_private_keys set; this should work |
| 118 | + self.nodes[1].createwallet(tempwallet, True) |
| 119 | + w = self.nodes[1].get_wallet_rpc(tempwallet) |
| 120 | + |
| 121 | + # Attempt to unset the disable_private_keys flag; this should not work |
| 122 | + assert_raises_rpc_error(-8, "Wallet flag is immutable", w.setwalletflag, 'disable_private_keys', False) |
| 123 | + |
| 124 | + # Unload temp wallet |
| 125 | + self.nodes[1].unloadwallet(tempwallet) |
| 126 | + |
| 127 | + def test_fund_send_fund_senddirty(self): |
| 128 | + ''' |
| 129 | + Test the same as test_fund_send_fund_send, except send the 10 BTC with |
| 130 | + the avoid_reuse flag set to false. This means the 10 BTC send should succeed, |
| 131 | + where it fails in test_fund_send_fund_send. |
| 132 | + ''' |
| 133 | + |
| 134 | + fundaddr = self.nodes[1].getnewaddress() |
| 135 | + retaddr = self.nodes[0].getnewaddress() |
| 136 | + |
| 137 | + self.nodes[0].sendtoaddress(fundaddr, 10) |
| 138 | + self.nodes[0].generate(1) |
| 139 | + self.sync_all() |
| 140 | + |
| 141 | + # listunspent should show 1 single, unused 10 btc output |
| 142 | + assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) |
| 143 | + |
| 144 | + self.nodes[1].sendtoaddress(retaddr, 5) |
| 145 | + self.nodes[0].generate(1) |
| 146 | + self.sync_all() |
| 147 | + |
| 148 | + # listunspent should show 1 single, unused 5 btc output |
| 149 | + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) |
| 150 | + |
| 151 | + self.nodes[0].sendtoaddress(fundaddr, 10) |
| 152 | + self.nodes[0].generate(1) |
| 153 | + self.sync_all() |
| 154 | + |
| 155 | + # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) |
| 156 | + assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) |
| 157 | + |
| 158 | + self.nodes[1].sendtoaddress(address=retaddr, amount=10, avoid_reuse=False) |
| 159 | + |
| 160 | + # listunspent should show 1 total outputs (5 btc), unused |
| 161 | + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_count=0) |
| 162 | + |
| 163 | + # node 1 should now have about 5 btc left (for both cases) |
| 164 | + assert_approx(self.nodes[1].getbalance(), 5, 0.001) |
| 165 | + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 5, 0.001) |
| 166 | + |
| 167 | + def test_fund_send_fund_send(self): |
| 168 | + ''' |
| 169 | + Test the simple case where [1] generates a new address A, then |
| 170 | + [0] sends 10 BTC to A. |
| 171 | + [1] spends 5 BTC from A. (leaving roughly 5 BTC useable) |
| 172 | + [0] sends 10 BTC to A again. |
| 173 | + [1] tries to spend 10 BTC (fails; dirty). |
| 174 | + [1] tries to spend 4 BTC (succeeds; change address sufficient) |
| 175 | + ''' |
| 176 | + |
| 177 | + fundaddr = self.nodes[1].getnewaddress() |
| 178 | + retaddr = self.nodes[0].getnewaddress() |
| 179 | + |
| 180 | + self.nodes[0].sendtoaddress(fundaddr, 10) |
| 181 | + self.nodes[0].generate(1) |
| 182 | + self.sync_all() |
| 183 | + |
| 184 | + # listunspent should show 1 single, unused 10 btc output |
| 185 | + assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) |
| 186 | + |
| 187 | + self.nodes[1].sendtoaddress(retaddr, 5) |
| 188 | + self.nodes[0].generate(1) |
| 189 | + self.sync_all() |
| 190 | + |
| 191 | + # listunspent should show 1 single, unused 5 btc output |
| 192 | + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) |
| 193 | + |
| 194 | + self.nodes[0].sendtoaddress(fundaddr, 10) |
| 195 | + self.nodes[0].generate(1) |
| 196 | + self.sync_all() |
| 197 | + |
| 198 | + # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) |
| 199 | + assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) |
| 200 | + |
| 201 | + # node 1 should now have a balance of 5 (no dirty) or 15 (including dirty) |
| 202 | + assert_approx(self.nodes[1].getbalance(), 5, 0.001) |
| 203 | + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 15, 0.001) |
| 204 | + |
| 205 | + assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10) |
| 206 | + |
| 207 | + self.nodes[1].sendtoaddress(retaddr, 4) |
| 208 | + |
| 209 | + # listunspent should show 2 total outputs (1, 10 btc), one unused (1), one reused (10) |
| 210 | + assert_unspent(self.nodes[1], total_count=2, total_sum=11, reused_count=1, reused_sum=10) |
| 211 | + |
| 212 | + # node 1 should now have about 1 btc left (no dirty) and 11 (including dirty) |
| 213 | + assert_approx(self.nodes[1].getbalance(), 1, 0.001) |
| 214 | + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001) |
| 215 | + |
| 216 | +if __name__ == '__main__': |
| 217 | + AvoidReuseTest().main() |
0 commit comments