Skip to content

Commit da03cb4

Browse files
committed
test: functional test for new coin selection logic
Create a wallet with mixed OutputTypes and send a volley of payments, ensuring that there are no mixed OutputTypes in the txs. Finally, verify that OutputTypes are mixed only when necessary.
1 parent 438e048 commit da03cb4

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
'mempool_spend_coinbase.py',
157157
'wallet_avoidreuse.py --legacy-wallet',
158158
'wallet_avoidreuse.py --descriptors',
159+
'wallet_avoid_mixing_output_types.py --descriptors',
159160
'mempool_reorg.py',
160161
'mempool_persist.py',
161162
'p2p_block_sync.py',
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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

Comments
 (0)