Skip to content

Commit fada6c6

Browse files
author
MarcoFalke
committed
wallet: Strictly match tx change type to improve privacy
1 parent 8c0bd87 commit fada6c6

File tree

5 files changed

+68
-53
lines changed

5 files changed

+68
-53
lines changed

src/wallet/wallet.cpp

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,29 +1967,58 @@ OutputType CWallet::TransactionChangeType(const std::optional<OutputType>& chang
19671967
return *change_type;
19681968
}
19691969

1970-
// if m_default_address_type is legacy, use legacy address as change (even
1971-
// if some of the outputs are P2WPKH or P2WSH).
1970+
// if m_default_address_type is legacy, use legacy address as change.
19721971
if (m_default_address_type == OutputType::LEGACY) {
19731972
return OutputType::LEGACY;
19741973
}
19751974

1976-
// if any destination is P2WPKH or P2WSH, use P2WPKH for the change
1977-
// output.
1975+
bool any_tr{false};
1976+
bool any_wpkh{false};
1977+
bool any_sh{false};
1978+
bool any_pkh{false};
1979+
19781980
for (const auto& recipient : vecSend) {
1979-
// Check if any destination contains a witness program:
1980-
int witnessversion = 0;
1981-
std::vector<unsigned char> witnessprogram;
1982-
if (recipient.scriptPubKey.IsWitnessProgram(witnessversion, witnessprogram)) {
1983-
if (GetScriptPubKeyMan(OutputType::BECH32M, true)) {
1984-
return OutputType::BECH32M;
1985-
} else if (GetScriptPubKeyMan(OutputType::BECH32, true)) {
1986-
return OutputType::BECH32;
1987-
} else {
1988-
return m_default_address_type;
1989-
}
1990-
}
1981+
std::vector<std::vector<uint8_t>> dummy;
1982+
const TxoutType type{Solver(recipient.scriptPubKey, dummy)};
1983+
if (type == TxoutType::WITNESS_V1_TAPROOT) {
1984+
any_tr = true;
1985+
} else if (type == TxoutType::WITNESS_V0_KEYHASH) {
1986+
any_wpkh = true;
1987+
} else if (type == TxoutType::SCRIPTHASH) {
1988+
any_sh = true;
1989+
} else if (type == TxoutType::PUBKEYHASH) {
1990+
any_pkh = true;
1991+
}
1992+
}
1993+
1994+
const bool has_bech32m_spkman(GetScriptPubKeyMan(OutputType::BECH32M, /*internal=*/true));
1995+
if (has_bech32m_spkman && any_tr) {
1996+
// Currently tr is the only type supported by the BECH32M spkman
1997+
return OutputType::BECH32M;
1998+
}
1999+
const bool has_bech32_spkman(GetScriptPubKeyMan(OutputType::BECH32, /*internal=*/true));
2000+
if (has_bech32_spkman && any_wpkh) {
2001+
// Currently wpkh is the only type supported by the BECH32 spkman
2002+
return OutputType::BECH32;
2003+
}
2004+
const bool has_p2sh_segwit_spkman(GetScriptPubKeyMan(OutputType::P2SH_SEGWIT, /*internal=*/true));
2005+
if (has_p2sh_segwit_spkman && any_sh) {
2006+
// Currently sh_wpkh is the only type supported by the P2SH_SEGWIT spkman
2007+
// As of 2021 about 80% of all SH are wrapping WPKH, so use that
2008+
return OutputType::P2SH_SEGWIT;
2009+
}
2010+
const bool has_legacy_spkman(GetScriptPubKeyMan(OutputType::LEGACY, /*internal=*/true));
2011+
if (has_legacy_spkman && any_pkh) {
2012+
// Currently pkh is the only type supported by the LEGACY spkman
2013+
return OutputType::LEGACY;
19912014
}
19922015

2016+
if (has_bech32m_spkman) {
2017+
return OutputType::BECH32M;
2018+
}
2019+
if (has_bech32_spkman) {
2020+
return OutputType::BECH32;
2021+
}
19932022
// else use m_default_address_type for change
19942023
return m_default_address_type;
19952024
}

test/functional/rpc_fundrawtransaction.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,12 @@ def test_locked_wallet(self):
571571
if self.options.descriptors:
572572
self.nodes[1].walletpassphrase('test', 10)
573573
self.nodes[1].importdescriptors([{
574-
'desc': descsum_create('tr(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'),
574+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'),
575575
'timestamp': 'now',
576576
'active': True
577577
},
578578
{
579-
'desc': descsum_create('tr(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'),
579+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'),
580580
'timestamp': 'now',
581581
'active': True,
582582
'internal': True
@@ -778,18 +778,11 @@ def test_option_feerate(self):
778778
for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]):
779779
assert_equal(self.nodes[3].fundrawtransaction(rawtx, {param: zero_value})["fee"], 0)
780780

781-
if self.options.descriptors:
782-
# With no arguments passed, expect fee of 153 satoshis as descriptor wallets now have a taproot output.
783-
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000153, vspan=0.00000001)
784-
# Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified.
785-
result = node.fundrawtransaction(rawtx, {"fee_rate": 10000})
786-
assert_approx(result["fee"], vexp=0.0153, vspan=0.0001)
787-
else:
788-
# With no arguments passed, expect fee of 141 satoshis as legacy wallets only support up to segwit v0.
789-
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
790-
# Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified.
791-
result = node.fundrawtransaction(rawtx, {"fee_rate": 10000})
792-
assert_approx(result["fee"], vexp=0.0141, vspan=0.0001)
781+
# With no arguments passed, expect fee of 141 satoshis.
782+
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
783+
# Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified.
784+
result = node.fundrawtransaction(rawtx, {"fee_rate": 10000})
785+
assert_approx(result["fee"], vexp=0.0141, vspan=0.0001)
793786

794787
self.log.info("Test fundrawtxn with invalid estimate_mode settings")
795788
for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
@@ -1080,7 +1073,7 @@ def test_22670(self):
10801073
# Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee
10811074
self.nodes[0].unloadwallet(self.default_wallet_name, False)
10821075
feerate = Decimal("0.1")
1083-
self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0", "-changetype=bech32", "-addresstype=bech32"]) # Set high minrelayfee, set discardfee to 0 for easier calculation
1076+
self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation
10841077

10851078
self.nodes[0].loadwallet(self.default_wallet_name, True)
10861079
funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name)

test/functional/wallet_address_types.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,13 @@ def run_test(self):
346346
self.test_change_output_type(0, [to_address_bech32_1], 'legacy')
347347

348348
if self.options.descriptors:
349-
self.log.info("Nodes with addresstype=p2sh-segwit only use a bech32m change output if any destination address is bech32:")
349+
self.log.info("Nodes with addresstype=p2sh-segwit match the change output")
350350
self.test_change_output_type(1, [to_address_p2sh], 'p2sh-segwit')
351-
self.test_change_output_type(1, [to_address_bech32_1], 'bech32m')
352-
self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32m')
353-
self.test_change_output_type(1, [to_address_bech32_1, to_address_bech32_2], 'bech32m')
351+
self.test_change_output_type(1, [to_address_bech32_1], 'bech32')
352+
self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32')
353+
self.test_change_output_type(1, [to_address_bech32_1, to_address_bech32_2], 'bech32')
354354
else:
355-
self.log.info("Nodes with addresstype=p2sh-segwit only use a P2WPKH change output if any destination address is bech32:")
355+
self.log.info("Nodes with addresstype=p2sh-segwit match the change output")
356356
self.test_change_output_type(1, [to_address_p2sh], 'p2sh-segwit')
357357
self.test_change_output_type(1, [to_address_bech32_1], 'bech32')
358358
self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32')
@@ -363,13 +363,13 @@ def run_test(self):
363363
self.test_change_output_type(2, [to_address_p2sh], 'bech32')
364364

365365
if self.options.descriptors:
366-
self.log.info("Nodes with addresstype=bech32 always use either a bech32 or bech32m change output (unless changetype is set otherwise):")
367-
self.test_change_output_type(3, [to_address_bech32_1], 'bech32m')
368-
self.test_change_output_type(3, [to_address_p2sh], 'bech32')
366+
self.log.info("Nodes with addresstype=bech32 match the change output (unless changetype is set otherwise):")
367+
self.test_change_output_type(3, [to_address_bech32_1], 'bech32')
368+
self.test_change_output_type(3, [to_address_p2sh], 'p2sh-segwit')
369369
else:
370-
self.log.info("Nodes with addresstype=bech32 always use a P2WPKH change output (unless changetype is set otherwise):")
370+
self.log.info("Nodes with addresstype=bech32 match the change output (unless changetype is set otherwise):")
371371
self.test_change_output_type(3, [to_address_bech32_1], 'bech32')
372-
self.test_change_output_type(3, [to_address_p2sh], 'bech32')
372+
self.test_change_output_type(3, [to_address_p2sh], 'p2sh-segwit')
373373

374374
self.log.info('getrawchangeaddress defaults to addresstype if -changetype is not set and argument is absent')
375375
self.test_address(3, self.nodes[3].getrawchangeaddress(), multisig=False, typ='bech32')

test/functional/wallet_groups.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,10 @@ def run_test(self):
108108
assert_equal(input_addrs[0], input_addrs[1])
109109
# Node 2 enforces avoidpartialspends so needs no checking here
110110

111-
if self.options.descriptors:
112-
# Descriptor wallets will use Taproot change by default which has different fees
113-
tx4_ungrouped_fee = 3060
114-
tx4_grouped_fee = 4400
115-
tx5_6_ungrouped_fee = 5760
116-
tx5_6_grouped_fee = 8480
117-
else:
118-
tx4_ungrouped_fee = 2820
119-
tx4_grouped_fee = 4160
120-
tx5_6_ungrouped_fee = 5520
121-
tx5_6_grouped_fee = 8240
111+
tx4_ungrouped_fee = 2820
112+
tx4_grouped_fee = 4160
113+
tx5_6_ungrouped_fee = 5520
114+
tx5_6_grouped_fee = 8240
122115

123116
self.log.info("Test wallet option maxapsfee")
124117
addr_aps = self.nodes[3].getnewaddress()

test/functional/wallet_hd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def run_test(self):
136136
keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath']
137137

138138
if self.options.descriptors:
139-
assert_equal(keypath[0:14], "m/86'/1'/0'/1/")
139+
assert_equal(keypath[0:14], "m/84'/1'/0'/1/")
140140
else:
141141
assert_equal(keypath[0:7], "m/0'/1'")
142142

0 commit comments

Comments
 (0)