|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or https://www.opensource.org/licenses/mit-license.php. |
| 5 | +"""Test output type mixing during coin selection |
| 6 | +
|
| 7 | +A wallet may have different types of UTXOs to choose from during coin selection, |
| 8 | +where output type is one of the following: |
| 9 | + - BECH32M |
| 10 | + - BECH32 |
| 11 | + - P2SH-SEGWIT |
| 12 | + - LEGACY |
| 13 | +
|
| 14 | +This test verifies that mixing different output types is avoided unless |
| 15 | +absolutely necessary. Both wallets start with zero funds. Alice mines |
| 16 | +enough blocks to have spendable coinbase outputs. Alice sends three |
| 17 | +random value payments which sum to 10BTC for each output type to Bob, |
| 18 | +for a total of 40BTC in Bob's wallet. |
| 19 | +
|
| 20 | +Bob then sends random valued payments back to Alice, some of which need |
| 21 | +unconfirmed change, and we verify that none of these payments contain mixed |
| 22 | +inputs. Finally, Bob sends the remainder of his funds, which requires mixing. |
| 23 | +
|
| 24 | +The payment values are random, but chosen such that they sum up to a specified |
| 25 | +total. This ensures we are not relying on specific values for the UTXOs, |
| 26 | +but still know when to expect mixing due to the wallet being close to empty. |
| 27 | +
|
| 28 | +""" |
| 29 | + |
| 30 | +import random |
| 31 | +from test_framework.test_framework import BitcoinTestFramework |
| 32 | +from test_framework.blocktools import COINBASE_MATURITY |
| 33 | + |
| 34 | +ADDRESS_TYPES = [ |
| 35 | + "bech32m", |
| 36 | + "bech32", |
| 37 | + "p2sh-segwit", |
| 38 | + "legacy", |
| 39 | +] |
| 40 | + |
| 41 | + |
| 42 | +def is_bech32_address(node, addr): |
| 43 | + """Check if an address contains a bech32 output.""" |
| 44 | + addr_info = node.getaddressinfo(addr) |
| 45 | + return addr_info['desc'].startswith('wpkh(') |
| 46 | + |
| 47 | + |
| 48 | +def is_bech32m_address(node, addr): |
| 49 | + """Check if an address contains a bech32m output.""" |
| 50 | + addr_info = node.getaddressinfo(addr) |
| 51 | + return addr_info['desc'].startswith('tr(') |
| 52 | + |
| 53 | + |
| 54 | +def is_p2sh_segwit_address(node, addr): |
| 55 | + """Check if an address contains a P2SH-Segwit output. |
| 56 | + Note: this function does not actually determine the type |
| 57 | + of P2SH output, but is sufficient for this test in that |
| 58 | + we are only generating P2SH-Segwit outputs. |
| 59 | + """ |
| 60 | + addr_info = node.getaddressinfo(addr) |
| 61 | + return addr_info['desc'].startswith('sh(wpkh(') |
| 62 | + |
| 63 | + |
| 64 | +def is_legacy_address(node, addr): |
| 65 | + """Check if an address contains a legacy output.""" |
| 66 | + addr_info = node.getaddressinfo(addr) |
| 67 | + return addr_info['desc'].startswith('pkh(') |
| 68 | + |
| 69 | + |
| 70 | +def is_same_type(node, tx): |
| 71 | + """Check that all inputs are of the same OutputType""" |
| 72 | + vins = node.getrawtransaction(tx, True)['vin'] |
| 73 | + inputs = [] |
| 74 | + for vin in vins: |
| 75 | + prev_tx, n = vin['txid'], vin['vout'] |
| 76 | + inputs.append( |
| 77 | + node.getrawtransaction( |
| 78 | + prev_tx, |
| 79 | + True, |
| 80 | + )['vout'][n]['scriptPubKey']['address'] |
| 81 | + ) |
| 82 | + has_legacy = False |
| 83 | + has_p2sh = False |
| 84 | + has_bech32 = False |
| 85 | + has_bech32m = False |
| 86 | + |
| 87 | + for addr in inputs: |
| 88 | + if is_legacy_address(node, addr): |
| 89 | + has_legacy = True |
| 90 | + if is_p2sh_segwit_address(node, addr): |
| 91 | + has_p2sh = True |
| 92 | + if is_bech32_address(node, addr): |
| 93 | + has_bech32 = True |
| 94 | + if is_bech32m_address(node, addr): |
| 95 | + has_bech32m = True |
| 96 | + |
| 97 | + return (sum([has_legacy, has_p2sh, has_bech32, has_bech32m]) == 1) |
| 98 | + |
| 99 | + |
| 100 | +def generate_payment_values(n, m): |
| 101 | + """Return a randomly chosen list of n positive integers summing to m. |
| 102 | + Each such list is equally likely to occur.""" |
| 103 | + |
| 104 | + dividers = sorted(random.sample(range(1, m), n - 1)) |
| 105 | + return [a - b for a, b in zip(dividers + [m], [0] + dividers)] |
| 106 | + |
| 107 | + |
| 108 | +class AddressInputTypeGrouping(BitcoinTestFramework): |
| 109 | + def set_test_params(self): |
| 110 | + self.setup_clean_chain = True |
| 111 | + self.num_nodes = 2 |
| 112 | + self.extra_args = [ |
| 113 | + [ |
| 114 | + "-addresstype=bech32", |
| 115 | + |
| 116 | + "-txindex", |
| 117 | + ], |
| 118 | + [ |
| 119 | + "-addresstype=p2sh-segwit", |
| 120 | + |
| 121 | + "-txindex", |
| 122 | + ], |
| 123 | + ] |
| 124 | + |
| 125 | + def skip_test_if_missing_module(self): |
| 126 | + self.skip_if_no_wallet() |
| 127 | + |
| 128 | + def make_payment(self, A, B, v, addr_type): |
| 129 | + fee_rate = random.randint(1, 20) |
| 130 | + self.log.debug(f"Making payment of {v} BTC at fee_rate {fee_rate}") |
| 131 | + tx = B.sendtoaddress( |
| 132 | + address=A.getnewaddress(address_type=addr_type), |
| 133 | + amount=v, |
| 134 | + fee_rate=fee_rate, |
| 135 | + ) |
| 136 | + return tx |
| 137 | + |
| 138 | + def run_test(self): |
| 139 | + |
| 140 | + # alias self.nodes[i] to A, B for readability |
| 141 | + A, B = self.nodes[0], self.nodes[1] |
| 142 | + self.generate(A, COINBASE_MATURITY + 5) |
| 143 | + |
| 144 | + self.log.info("Creating mixed UTXOs in B's wallet") |
| 145 | + for v in generate_payment_values(3, 10): |
| 146 | + self.log.debug(f"Making payment of {v} BTC to legacy") |
| 147 | + A.sendtoaddress(B.getnewaddress(address_type="legacy"), v) |
| 148 | + |
| 149 | + for v in generate_payment_values(3, 10): |
| 150 | + self.log.debug(f"Making payment of {v} BTC to p2sh") |
| 151 | + A.sendtoaddress(B.getnewaddress(address_type="p2sh-segwit"), v) |
| 152 | + |
| 153 | + for v in generate_payment_values(3, 10): |
| 154 | + self.log.debug(f"Making payment of {v} BTC to bech32") |
| 155 | + A.sendtoaddress(B.getnewaddress(address_type="bech32"), v) |
| 156 | + |
| 157 | + for v in generate_payment_values(3, 10): |
| 158 | + self.log.debug(f"Making payment of {v} BTC to bech32m") |
| 159 | + A.sendtoaddress(B.getnewaddress(address_type="bech32m"), v) |
| 160 | + |
| 161 | + self.generate(A, 1) |
| 162 | + |
| 163 | + self.log.info("Sending payments from B to A") |
| 164 | + for v in generate_payment_values(5, 9): |
| 165 | + tx = self.make_payment( |
| 166 | + A, B, v, random.choice(ADDRESS_TYPES) |
| 167 | + ) |
| 168 | + self.generate(A, 1) |
| 169 | + assert is_same_type(B, tx) |
| 170 | + |
| 171 | + tx = self.make_payment(A, B, 30.99, random.choice(ADDRESS_TYPES)) |
| 172 | + assert not is_same_type(B, tx) |
| 173 | + |
| 174 | + |
| 175 | +if __name__ == '__main__': |
| 176 | + AddressInputTypeGrouping().main() |
0 commit comments