Skip to content

Commit 711d16c

Browse files
committed
Merge #11667: Add scripts to dumpwallet RPC
656fde5 Add script birthtime metadata to dump and import wallet (MeshCollider) 1bab9b2 Add script dump note to RPC help text and release notes (MeshCollider) 68c1e00 Add test for importwallet (MeshCollider) 9e1184d Add dumpwallet scripts test (MeshCollider) ef0c730 Add scripts to importwallet RPC (MeshCollider) b702ae8 Add CScripts to dumpwallet RPC (MeshCollider) cdc260a Add GetCScripts to CBasicKeyStore (MeshCollider) Pull request description: As discussed in bitcoin/bitcoin#11289 (comment), adds the CScripts from the wallet to the `dumpwallet` RPC and then allows them to be imported with the `importwallet` RPC. Includes a basic test, and modifies the helptext of the dumpwallet RPC. Notes: - Reviewers: use `?w=1` to avoid the indentation-only change in commit `Add scripts to importwallet RPC ` - currently the scripts are followed with `# addr=` comments just as the other keys are, unsure if this might confuse users into thinking all the scripts are for valid P2SH addresses though, but I don't think that should be an issue. - there are no birthtimes for scripts, so script imports don't affect rescans - `importwallet` imports the CScripts but I'm not sure how to approach specifying whether scripts are for P2SH addresses, BIP173 addresses, etc. whether that matters or not. Otherwise the RPC helptext might just need modification. Fixes #11715 Tree-SHA512: 36c55837b3a58b9d3499d4c0c2ae82153d62aa71919e751574651b63a1d2b8ecc83796db4553cc65dad9b5341c3a42ae2fcf4d62598c30af267f8e1461ba8272
2 parents 7a11ba7 + 656fde5 commit 711d16c

File tree

5 files changed

+123
-40
lines changed

5 files changed

+123
-40
lines changed

doc/release-notes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ The `share/rpcuser/rpcuser.py` script was renamed to `share/rpcauth/rpcauth.py`.
100100
used to create `rpcauth` credentials for a JSON-RPC user.
101101

102102

103+
- `dumpwallet` now includes hex-encoded scripts from the wallet in the dumpfile, and
104+
`importwallet` now imports these scripts, but corresponding addresses may not be added
105+
correctly or a manual rescan may be required to find relevant transactions.
106+
103107
Credits
104108
=======
105109

src/keystore.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ bool CBasicKeyStore::HaveCScript(const CScriptID& hash) const
7777
return mapScripts.count(hash) > 0;
7878
}
7979

80+
std::set<CScriptID> CBasicKeyStore::GetCScripts() const
81+
{
82+
LOCK(cs_KeyStore);
83+
std::set<CScriptID> set_script;
84+
for (const auto& mi : mapScripts) {
85+
set_script.insert(mi.first);
86+
}
87+
return set_script;
88+
}
89+
8090
bool CBasicKeyStore::GetCScript(const CScriptID &hash, CScript& redeemScriptOut) const
8191
{
8292
LOCK(cs_KeyStore);

src/keystore.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class CKeyStore
3636
//! Support for BIP 0013 : see https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki
3737
virtual bool AddCScript(const CScript& redeemScript) =0;
3838
virtual bool HaveCScript(const CScriptID &hash) const =0;
39+
virtual std::set<CScriptID> GetCScripts() const =0;
3940
virtual bool GetCScript(const CScriptID &hash, CScript& redeemScriptOut) const =0;
4041

4142
//! Support for Watch-only addresses
@@ -67,6 +68,7 @@ class CBasicKeyStore : public CKeyStore
6768
bool GetKey(const CKeyID &address, CKey &keyOut) const override;
6869
bool AddCScript(const CScript& redeemScript) override;
6970
bool HaveCScript(const CScriptID &hash) const override;
71+
std::set<CScriptID> GetCScripts() const override;
7072
bool GetCScript(const CScriptID &hash, CScript& redeemScriptOut) const override;
7173

7274
bool AddWatchOnly(const CScript &dest) override;

src/wallet/rpcdump.cpp

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -500,40 +500,57 @@ UniValue importwallet(const JSONRPCRequest& request)
500500
if (vstr.size() < 2)
501501
continue;
502502
CBitcoinSecret vchSecret;
503-
if (!vchSecret.SetString(vstr[0]))
504-
continue;
505-
CKey key = vchSecret.GetKey();
506-
CPubKey pubkey = key.GetPubKey();
507-
assert(key.VerifyPubKey(pubkey));
508-
CKeyID keyid = pubkey.GetID();
509-
if (pwallet->HaveKey(keyid)) {
510-
LogPrintf("Skipping import of %s (key already present)\n", EncodeDestination(keyid));
511-
continue;
512-
}
513-
int64_t nTime = DecodeDumpTime(vstr[1]);
514-
std::string strLabel;
515-
bool fLabel = true;
516-
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
517-
if (boost::algorithm::starts_with(vstr[nStr], "#"))
518-
break;
519-
if (vstr[nStr] == "change=1")
520-
fLabel = false;
521-
if (vstr[nStr] == "reserve=1")
522-
fLabel = false;
523-
if (boost::algorithm::starts_with(vstr[nStr], "label=")) {
524-
strLabel = DecodeDumpString(vstr[nStr].substr(6));
525-
fLabel = true;
503+
if (vchSecret.SetString(vstr[0])) {
504+
CKey key = vchSecret.GetKey();
505+
CPubKey pubkey = key.GetPubKey();
506+
assert(key.VerifyPubKey(pubkey));
507+
CKeyID keyid = pubkey.GetID();
508+
if (pwallet->HaveKey(keyid)) {
509+
LogPrintf("Skipping import of %s (key already present)\n", EncodeDestination(keyid));
510+
continue;
526511
}
512+
int64_t nTime = DecodeDumpTime(vstr[1]);
513+
std::string strLabel;
514+
bool fLabel = true;
515+
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
516+
if (boost::algorithm::starts_with(vstr[nStr], "#"))
517+
break;
518+
if (vstr[nStr] == "change=1")
519+
fLabel = false;
520+
if (vstr[nStr] == "reserve=1")
521+
fLabel = false;
522+
if (boost::algorithm::starts_with(vstr[nStr], "label=")) {
523+
strLabel = DecodeDumpString(vstr[nStr].substr(6));
524+
fLabel = true;
525+
}
526+
}
527+
LogPrintf("Importing %s...\n", EncodeDestination(keyid));
528+
if (!pwallet->AddKeyPubKey(key, pubkey)) {
529+
fGood = false;
530+
continue;
531+
}
532+
pwallet->mapKeyMetadata[keyid].nCreateTime = nTime;
533+
if (fLabel)
534+
pwallet->SetAddressBook(keyid, strLabel, "receive");
535+
nTimeBegin = std::min(nTimeBegin, nTime);
536+
} else if(IsHex(vstr[0])) {
537+
std::vector<unsigned char> vData(ParseHex(vstr[0]));
538+
CScript script = CScript(vData.begin(), vData.end());
539+
if (pwallet->HaveCScript(script)) {
540+
LogPrintf("Skipping import of %s (script already present)\n", vstr[0]);
541+
continue;
542+
}
543+
if(!pwallet->AddCScript(script)) {
544+
LogPrintf("Error importing script %s\n", vstr[0]);
545+
fGood = false;
546+
continue;
547+
}
548+
int64_t birth_time = DecodeDumpTime(vstr[1]);
549+
if (birth_time > 0) {
550+
pwallet->m_script_metadata[CScriptID(script)].nCreateTime = birth_time;
551+
nTimeBegin = std::min(nTimeBegin, birth_time);
552+
}
527553
}
528-
LogPrintf("Importing %s...\n", EncodeDestination(keyid));
529-
if (!pwallet->AddKeyPubKey(key, pubkey)) {
530-
fGood = false;
531-
continue;
532-
}
533-
pwallet->mapKeyMetadata[keyid].nCreateTime = nTime;
534-
if (fLabel)
535-
pwallet->SetAddressBook(keyid, strLabel, "receive");
536-
nTimeBegin = std::min(nTimeBegin, nTime);
537554
}
538555
file.close();
539556
pwallet->ShowProgress("", 100); // hide progress dialog in GUI
@@ -542,7 +559,7 @@ UniValue importwallet(const JSONRPCRequest& request)
542559
pwallet->MarkDirty();
543560

544561
if (!fGood)
545-
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
562+
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys/scripts to wallet");
546563

547564
return NullUniValue;
548565
}
@@ -601,7 +618,7 @@ UniValue dumpwallet(const JSONRPCRequest& request)
601618
throw std::runtime_error(
602619
"dumpwallet \"filename\"\n"
603620
"\nDumps all wallet keys in a human-readable format to a server-side file. This does not allow overwriting existing files.\n"
604-
"Imported scripts are not currently included in wallet dumps, these must be backed up separately.\n"
621+
"Imported scripts are included in the dumpfile, but corresponding BIP173 addresses, etc. may not be added automatically by importwallet.\n"
605622
"Note that if your wallet contains keys which are not derived from your HD seed (e.g. imported keys), these are not covered by\n"
606623
"only backing up the seed itself, and must be backed up too (e.g. ensure you back up the whole dumpfile).\n"
607624
"\nArguments:\n"
@@ -640,6 +657,9 @@ UniValue dumpwallet(const JSONRPCRequest& request)
640657
const std::map<CKeyID, int64_t>& mapKeyPool = pwallet->GetAllReserveKeys();
641658
pwallet->GetKeyBirthTimes(mapKeyBirth);
642659

660+
std::set<CScriptID> scripts = pwallet->GetCScripts();
661+
// TODO: include scripts in GetKeyBirthTimes() output instead of separate
662+
643663
// sort time/key pairs
644664
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
645665
for (const auto& entry : mapKeyBirth) {
@@ -694,6 +714,21 @@ UniValue dumpwallet(const JSONRPCRequest& request)
694714
}
695715
}
696716
file << "\n";
717+
for (const CScriptID &scriptid : scripts) {
718+
CScript script;
719+
std::string create_time = "0";
720+
std::string address = EncodeDestination(scriptid);
721+
// get birth times for scripts with metadata
722+
auto it = pwallet->m_script_metadata.find(scriptid);
723+
if (it != pwallet->m_script_metadata.end()) {
724+
create_time = EncodeDumpTime(it->second.nCreateTime);
725+
}
726+
if(pwallet->GetCScript(scriptid, script)) {
727+
file << strprintf("%s %s script=1", HexStr(script.begin(), script.end()), create_time);
728+
file << strprintf(" # addr=%s\n", address);
729+
}
730+
}
731+
file << "\n";
697732
file << "# End of dump\n";
698733
file.close();
699734

test/functional/wallet-dump.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
from test_framework.util import (assert_equal, assert_raises_rpc_error)
1111

1212

13-
def read_dump(file_name, addrs, hd_master_addr_old):
13+
def read_dump(file_name, addrs, script_addrs, hd_master_addr_old):
1414
"""
1515
Read the given dump, count the addrs that match, count change and reserve.
1616
Also check that the old hd_master is inactive
1717
"""
1818
with open(file_name, encoding='utf8') as inputfile:
1919
found_addr = 0
20+
found_script_addr = 0
2021
found_addr_chg = 0
2122
found_addr_rsv = 0
2223
hd_master_addr_ret = None
@@ -38,6 +39,9 @@ def read_dump(file_name, addrs, hd_master_addr_old):
3839
# ensure we have generated a new hd master key
3940
assert(hd_master_addr_old != addr)
4041
hd_master_addr_ret = addr
42+
elif keytype == "script=1":
43+
# scripts don't have keypaths
44+
keypath = None
4145
else:
4246
keypath = addr_keypath.rstrip().split("hdkeypath=")[1]
4347

@@ -52,7 +56,14 @@ def read_dump(file_name, addrs, hd_master_addr_old):
5256
elif keytype == "reserve=1":
5357
found_addr_rsv += 1
5458
break
55-
return found_addr, found_addr_chg, found_addr_rsv, hd_master_addr_ret
59+
60+
# count scripts
61+
for script_addr in script_addrs:
62+
if script_addr == addr.rstrip() and keytype == "script=1":
63+
found_script_addr += 1
64+
break
65+
66+
return found_addr, found_script_addr, found_addr_chg, found_addr_rsv, hd_master_addr_ret
5667

5768

5869
class WalletDumpTest(BitcoinTestFramework):
@@ -81,13 +92,19 @@ def run_test (self):
8192
# Should be a no-op:
8293
self.nodes[0].keypoolrefill()
8394

95+
# Test scripts dump by adding a P2SH witness and a 1-of-1 multisig address
96+
witness_addr = self.nodes[0].addwitnessaddress(addrs[0]["address"], True)
97+
multisig_addr = self.nodes[0].addmultisigaddress(1, [addrs[1]["address"]])
98+
script_addrs = [witness_addr, multisig_addr]
99+
84100
# dump unencrypted wallet
85101
result = self.nodes[0].dumpwallet(tmpdir + "/node0/wallet.unencrypted.dump")
86102
assert_equal(result['filename'], os.path.abspath(tmpdir + "/node0/wallet.unencrypted.dump"))
87103

88-
found_addr, found_addr_chg, found_addr_rsv, hd_master_addr_unenc = \
89-
read_dump(tmpdir + "/node0/wallet.unencrypted.dump", addrs, None)
104+
found_addr, found_script_addr, found_addr_chg, found_addr_rsv, hd_master_addr_unenc = \
105+
read_dump(tmpdir + "/node0/wallet.unencrypted.dump", addrs, script_addrs, None)
90106
assert_equal(found_addr, test_addr_count) # all keys must be in the dump
107+
assert_equal(found_script_addr, 2) # all scripts must be in the dump
91108
assert_equal(found_addr_chg, 50) # 50 blocks where mined
92109
assert_equal(found_addr_rsv, 90*2) # 90 keys plus 100% internal keys
93110

@@ -99,14 +116,29 @@ def run_test (self):
99116
self.nodes[0].keypoolrefill()
100117
self.nodes[0].dumpwallet(tmpdir + "/node0/wallet.encrypted.dump")
101118

102-
found_addr, found_addr_chg, found_addr_rsv, _ = \
103-
read_dump(tmpdir + "/node0/wallet.encrypted.dump", addrs, hd_master_addr_unenc)
119+
found_addr, found_script_addr, found_addr_chg, found_addr_rsv, _ = \
120+
read_dump(tmpdir + "/node0/wallet.encrypted.dump", addrs, script_addrs, hd_master_addr_unenc)
104121
assert_equal(found_addr, test_addr_count)
122+
assert_equal(found_script_addr, 2)
105123
assert_equal(found_addr_chg, 90*2 + 50) # old reserve keys are marked as change now
106124
assert_equal(found_addr_rsv, 90*2)
107125

108126
# Overwriting should fail
109127
assert_raises_rpc_error(-8, "already exists", self.nodes[0].dumpwallet, tmpdir + "/node0/wallet.unencrypted.dump")
110128

129+
# Restart node with new wallet, and test importwallet
130+
self.stop_node(0)
131+
self.start_node(0, ['-wallet=w2'])
132+
133+
# Make sure the address is not IsMine before import
134+
result = self.nodes[0].validateaddress(multisig_addr)
135+
assert(result['ismine'] == False)
136+
137+
self.nodes[0].importwallet(os.path.abspath(tmpdir + "/node0/wallet.unencrypted.dump"))
138+
139+
# Now check IsMine is true
140+
result = self.nodes[0].validateaddress(multisig_addr)
141+
assert(result['ismine'] == True)
142+
111143
if __name__ == '__main__':
112144
WalletDumpTest().main ()

0 commit comments

Comments
 (0)