Skip to content

Commit 68e841e

Browse files
committed
Merge #18067: wallet: Improve LegacyScriptPubKeyMan::CanProvide script recognition
a304a36 Revert "Store p2sh scripts in AddAndGetDestinationForScript" (Russell Yanofsky) eb7d8a5 [test] check for addmultisigaddress regression (Sjors Provoost) 005f8a9 wallet: Improve LegacyScriptPubKeyMan::CanProvide script recognition (Russell Yanofsky) Pull request description: Make `LegacyScriptPubKeyMan::CanProvide` method able to recognize p2sh scripts when the redeem script is present in the `mapScripts` map without the p2sh script also having to be added to the `mapScripts` map. This restores behavior prior to #17261, which I think broke backwards compatibility with old wallet files by no longer treating addresses created by `addmultisigaddress` calls before #17261 as solvable. The reason why tests didn't fail with the CanProvide implementation in #17261 is because of a workaround added in 4a7e43e "Store p2sh scripts in AddAndGetDestinationForScript", which masked the problem for new `addmultisigaddress` RPC calls without fixing it for multisig addresses already created in old wallet files. This change adds a lot of comments and allows reverting commit 4a7e43e "Store p2sh scripts in AddAndGetDestinationForScript", so the `AddAndGetDestinationForScript()` function, `CanProvide()` method, and `mapScripts` map should all be more comprehensible ACKs for top commit: Sjors: re-ACK a304a36 (rebase, slight text changes and my test) achow101: re-ACK a304a36 meshcollider: utACK a304a36 Tree-SHA512: 03b625220c49684c376a8062d7646aeba0e5bfe043f977dc7dc357a6754627d594e070e4d458d12d2291888405d94c1dbe08c7787c318374cedd5755e724fb6e
2 parents 36f42e1 + a304a36 commit 68e841e

File tree

6 files changed

+123
-21
lines changed

6 files changed

+123
-21
lines changed

src/Makefile.test.include

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ BITCOIN_TESTS += \
209209
wallet/test/wallet_crypto_tests.cpp \
210210
wallet/test/coinselector_tests.cpp \
211211
wallet/test/init_tests.cpp \
212-
wallet/test/ismine_tests.cpp
212+
wallet/test/ismine_tests.cpp \
213+
wallet/test/scriptpubkeyman_tests.cpp
213214

214215
BITCOIN_TEST_SUITE += \
215216
wallet/test/wallet_test_fixture.cpp \

src/outputtype.cpp

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,30 +82,22 @@ CTxDestination AddAndGetDestinationForScript(FillableSigningProvider& keystore,
8282
{
8383
// Add script to keystore
8484
keystore.AddCScript(script);
85-
ScriptHash sh(script);
8685
// Note that scripts over 520 bytes are not yet supported.
8786
switch (type) {
8887
case OutputType::LEGACY:
89-
keystore.AddCScript(GetScriptForDestination(sh));
90-
return sh;
88+
return ScriptHash(script);
9189
case OutputType::P2SH_SEGWIT:
9290
case OutputType::BECH32: {
9391
CTxDestination witdest = WitnessV0ScriptHash(script);
9492
CScript witprog = GetScriptForDestination(witdest);
9593
// Check if the resulting program is solvable (i.e. doesn't use an uncompressed key)
96-
if (!IsSolvable(keystore, witprog)) {
97-
// Since the wsh is invalid, add and return the sh instead.
98-
keystore.AddCScript(GetScriptForDestination(sh));
99-
return sh;
100-
}
94+
if (!IsSolvable(keystore, witprog)) return ScriptHash(script);
10195
// Add the redeemscript, so that P2WSH and P2SH-P2WSH outputs are recognized as ours.
10296
keystore.AddCScript(witprog);
10397
if (type == OutputType::BECH32) {
10498
return witdest;
10599
} else {
106-
ScriptHash sh_w = ScriptHash(witprog);
107-
keystore.AddCScript(GetScriptForDestination(sh_w));
108-
return sh_w;
100+
return ScriptHash(witprog);
109101
}
110102
}
111103
default: assert(false);

src/script/signingprovider.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,53 @@ class FillableSigningProvider : public SigningProvider
6666
using KeyMap = std::map<CKeyID, CKey>;
6767
using ScriptMap = std::map<CScriptID, CScript>;
6868

69+
/**
70+
* Map of key id to unencrypted private keys known by the signing provider.
71+
* Map may be empty if the provider has another source of keys, like an
72+
* encrypted store.
73+
*/
6974
KeyMap mapKeys GUARDED_BY(cs_KeyStore);
75+
76+
/**
77+
* Map of script id to scripts known by the signing provider.
78+
*
79+
* This map originally just held P2SH redeemScripts, and was used by wallet
80+
* code to look up script ids referenced in "OP_HASH160 <script id>
81+
* OP_EQUAL" P2SH outputs. Later in 605e8473a7d it was extended to hold
82+
* P2WSH witnessScripts as well, and used to look up nested scripts
83+
* referenced in "OP_0 <script hash>" P2WSH outputs. Later in commits
84+
* f4691ab3a9d and 248f3a76a82, it was extended once again to hold segwit
85+
* "OP_0 <key or script hash>" scriptPubKeys, in order to give the wallet a
86+
* way to distinguish between segwit outputs that it generated addresses for
87+
* and wanted to receive payments from, and segwit outputs that it never
88+
* generated addresses for, but it could spend just because of having keys.
89+
* (Before segwit activation it was also important to not treat segwit
90+
* outputs to arbitrary wallet keys as payments, because these could be
91+
* spent by anyone without even needing to sign with the keys.)
92+
*
93+
* Some of the scripts stored in mapScripts are memory-only and
94+
* intentionally not saved to disk. Specifically, scripts added by
95+
* ImplicitlyLearnRelatedKeyScripts(pubkey) calls are not written to disk so
96+
* future wallet code can have flexibility to be more selective about what
97+
* transaction outputs it recognizes as payments, instead of having to treat
98+
* all outputs spending to keys it knows as payments. By contrast,
99+
* mapScripts entries added by AddCScript(script),
100+
* LearnRelatedScripts(pubkey, type), and LearnAllRelatedScripts(pubkey)
101+
* calls are saved because they are all intentionally used to receive
102+
* payments.
103+
*
104+
* The FillableSigningProvider::mapScripts script map should not be confused
105+
* with LegacyScriptPubKeyMan::setWatchOnly script set. The two collections
106+
* can hold the same scripts, but they serve different purposes. The
107+
* setWatchOnly script set is intended to expand the set of outputs the
108+
* wallet considers payments. Every output with a script it contains is
109+
* considered to belong to the wallet, regardless of whether the script is
110+
* solvable or signable. By contrast, the scripts in mapScripts are only
111+
* used for solving, and to restrict which outputs are considered payments
112+
* by the wallet. An output with a script in mapScripts, unlike
113+
* setWatchOnly, is not automatically considered to belong to the wallet if
114+
* it can't be solved and signed for.
115+
*/
70116
ScriptMap mapScripts GUARDED_BY(cs_KeyStore);
71117

72118
void ImplicitlyLearnRelatedKeyScripts(const CPubKey& pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);

src/wallet/scriptpubkeyman.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,15 @@ bool HaveKeys(const std::vector<valtype>& pubkeys, const LegacyScriptPubKeyMan&
7070
return true;
7171
}
7272

73-
IsMineResult IsMineInner(const LegacyScriptPubKeyMan& keystore, const CScript& scriptPubKey, IsMineSigVersion sigversion)
73+
//! Recursively solve script and return spendable/watchonly/invalid status.
74+
//!
75+
//! @param keystore legacy key and script store
76+
//! @param script script to solve
77+
//! @param sigversion script type (top-level / redeemscript / witnessscript)
78+
//! @param recurse_scripthash whether to recurse into nested p2sh and p2wsh
79+
//! scripts or simply treat any script that has been
80+
//! stored in the keystore as spendable
81+
IsMineResult IsMineInner(const LegacyScriptPubKeyMan& keystore, const CScript& scriptPubKey, IsMineSigVersion sigversion, bool recurse_scripthash=true)
7482
{
7583
IsMineResult ret = IsMineResult::NO;
7684

@@ -129,7 +137,7 @@ IsMineResult IsMineInner(const LegacyScriptPubKeyMan& keystore, const CScript& s
129137
CScriptID scriptID = CScriptID(uint160(vSolutions[0]));
130138
CScript subscript;
131139
if (keystore.GetCScript(scriptID, subscript)) {
132-
ret = std::max(ret, IsMineInner(keystore, subscript, IsMineSigVersion::P2SH));
140+
ret = std::max(ret, recurse_scripthash ? IsMineInner(keystore, subscript, IsMineSigVersion::P2SH) : IsMineResult::SPENDABLE);
133141
}
134142
break;
135143
}
@@ -147,7 +155,7 @@ IsMineResult IsMineInner(const LegacyScriptPubKeyMan& keystore, const CScript& s
147155
CScriptID scriptID = CScriptID(hash);
148156
CScript subscript;
149157
if (keystore.GetCScript(scriptID, subscript)) {
150-
ret = std::max(ret, IsMineInner(keystore, subscript, IsMineSigVersion::WITNESS_V0));
158+
ret = std::max(ret, recurse_scripthash ? IsMineInner(keystore, subscript, IsMineSigVersion::WITNESS_V0) : IsMineResult::SPENDABLE);
151159
}
152160
break;
153161
}
@@ -476,11 +484,11 @@ std::unique_ptr<SigningProvider> LegacyScriptPubKeyMan::GetSigningProvider(const
476484

477485
bool LegacyScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sigdata)
478486
{
479-
if (IsMine(script) != ISMINE_NO) {
480-
// If it IsMine, we can always provide in some way
481-
return true;
482-
} else if (HaveCScript(CScriptID(script))) {
483-
// We can still provide some stuff if we have the script, but IsMine failed because we don't have keys
487+
IsMineResult ismine = IsMineInner(*this, script, IsMineSigVersion::TOP, /* recurse_scripthash= */ false);
488+
if (ismine == IsMineResult::SPENDABLE || ismine == IsMineResult::WATCH_ONLY) {
489+
// If ismine, it means we recognize keys or script ids in the script, or
490+
// are watching the script itself, and we can at least provide metadata
491+
// or solving information, even if not able to sign fully.
484492
return true;
485493
} else {
486494
// If, given the stuff in sigdata, we could make a valid sigature, then we can provide for this script
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2020 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <key.h>
6+
#include <script/standard.h>
7+
#include <test/util/setup_common.h>
8+
#include <wallet/scriptpubkeyman.h>
9+
#include <wallet/wallet.h>
10+
11+
#include <boost/test/unit_test.hpp>
12+
13+
BOOST_FIXTURE_TEST_SUITE(scriptpubkeyman_tests, BasicTestingSetup)
14+
15+
// Test LegacyScriptPubKeyMan::CanProvide behavior, making sure it returns true
16+
// for recognized scripts even when keys may not be available for signing.
17+
BOOST_AUTO_TEST_CASE(CanProvide)
18+
{
19+
// Set up wallet and keyman variables.
20+
NodeContext node;
21+
std::unique_ptr<interfaces::Chain> chain = interfaces::MakeChain(node);
22+
CWallet wallet(chain.get(), WalletLocation(), WalletDatabase::CreateDummy());
23+
LegacyScriptPubKeyMan& keyman = *wallet.GetOrCreateLegacyScriptPubKeyMan();
24+
25+
// Make a 1 of 2 multisig script
26+
std::vector<CKey> keys(2);
27+
std::vector<CPubKey> pubkeys;
28+
for (CKey& key : keys) {
29+
key.MakeNewKey(true);
30+
pubkeys.emplace_back(key.GetPubKey());
31+
}
32+
CScript multisig_script = GetScriptForMultisig(1, pubkeys);
33+
CScript p2sh_script = GetScriptForDestination(ScriptHash(multisig_script));
34+
SignatureData data;
35+
36+
// Verify the p2sh(multisig) script is not recognized until the multisig
37+
// script is added to the keystore to make it solvable
38+
BOOST_CHECK(!keyman.CanProvide(p2sh_script, data));
39+
keyman.AddCScript(multisig_script);
40+
BOOST_CHECK(keyman.CanProvide(p2sh_script, data));
41+
}
42+
43+
BOOST_AUTO_TEST_SUITE_END()

test/functional/feature_backwards_compatibility.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ def run_test(self):
122122
info = wallet.getwalletinfo()
123123
assert info['private_keys_enabled']
124124
assert info['keypoolsize'] > 0
125+
# Use addmultisigaddress (see #18075)
126+
address_18075 = wallet.addmultisigaddress(1, ["0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52", "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073"], "", "legacy")["address"]
127+
assert wallet.getaddressinfo(address_18075)["solvable"]
125128

126129
# w1_v18: regular wallet, created with v0.18
127130
node_v18.createwallet(wallet_name="w1_v18")
@@ -319,7 +322,7 @@ def run_test(self):
319322
hdkeypath = info["hdkeypath"]
320323
pubkey = info["pubkey"]
321324

322-
# Copy the wallet to the last Bitcoin Core version and open it:
325+
# Copy the 0.17 wallet to the last Bitcoin Core version and open it:
323326
node_v17.unloadwallet("u1_v17")
324327
shutil.copytree(
325328
os.path.join(node_v17_wallets_dir, "u1_v17"),
@@ -331,5 +334,14 @@ def run_test(self):
331334
descriptor = "wpkh([" + info["hdmasterfingerprint"] + hdkeypath[1:] + "]" + pubkey + ")"
332335
assert_equal(info["desc"], descsum_create(descriptor))
333336

337+
# Copy the 0.19 wallet to the last Bitcoin Core version and open it:
338+
shutil.copytree(
339+
os.path.join(node_v19_wallets_dir, "w1_v19"),
340+
os.path.join(node_master_wallets_dir, "w1_v19")
341+
)
342+
node_master.loadwallet("w1_v19")
343+
wallet = node_master.get_wallet_rpc("w1_v19")
344+
assert wallet.getaddressinfo(address_18075)["solvable"]
345+
334346
if __name__ == '__main__':
335347
BackwardsCompatibilityTest().main()

0 commit comments

Comments
 (0)