Skip to content

Commit a80f98b

Browse files
committed
Use importmulti timestamp when importing watch only keys
When importing a watch-only address over importmulti with a specific timestamp, the wallet's nTimeFirstKey is currently set to 1. After this change, the provided timestamp will be used and stored as metadata associated with watch-only key. This can improve wallet performance because it can avoid the need to scan the entire blockchain for watch only addresses when timestamps are provided. Also adds timestamp to validateaddress return value (needed for tests). Fixes #9034.
1 parent a58370e commit a80f98b

File tree

7 files changed

+107
-35
lines changed

7 files changed

+107
-35
lines changed

qa/rpc-tests/importmulti.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def run_test (self):
2020
print ("Mining blocks...")
2121
self.nodes[0].generate(1)
2222
self.nodes[1].generate(1)
23+
timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime']
2324

2425
# keyword definition
2526
PRIV_KEY = 'privkey'
@@ -59,6 +60,9 @@ def run_test (self):
5960
address_assert = self.nodes[1].validateaddress(address['address'])
6061
assert_equal(address_assert['iswatchonly'], True)
6162
assert_equal(address_assert['ismine'], False)
63+
assert_equal(address_assert['timestamp'], timestamp)
64+
watchonly_address = address['address']
65+
watchonly_timestamp = timestamp
6266

6367

6468
# ScriptPubKey + internal
@@ -73,6 +77,7 @@ def run_test (self):
7377
address_assert = self.nodes[1].validateaddress(address['address'])
7478
assert_equal(address_assert['iswatchonly'], True)
7579
assert_equal(address_assert['ismine'], False)
80+
assert_equal(address_assert['timestamp'], timestamp)
7681

7782
# ScriptPubKey + !internal
7883
print("Should not import a scriptPubKey without internal flag")
@@ -87,6 +92,7 @@ def run_test (self):
8792
address_assert = self.nodes[1].validateaddress(address['address'])
8893
assert_equal(address_assert['iswatchonly'], False)
8994
assert_equal(address_assert['ismine'], False)
95+
assert_equal('timestamp' in address_assert, False)
9096

9197

9298
# Address + Public key + !Internal
@@ -103,6 +109,7 @@ def run_test (self):
103109
address_assert = self.nodes[1].validateaddress(address['address'])
104110
assert_equal(address_assert['iswatchonly'], True)
105111
assert_equal(address_assert['ismine'], False)
112+
assert_equal(address_assert['timestamp'], timestamp)
106113

107114

108115
# ScriptPubKey + Public key + internal
@@ -119,6 +126,7 @@ def run_test (self):
119126
address_assert = self.nodes[1].validateaddress(address['address'])
120127
assert_equal(address_assert['iswatchonly'], True)
121128
assert_equal(address_assert['ismine'], False)
129+
assert_equal(address_assert['timestamp'], timestamp)
122130

123131
# ScriptPubKey + Public key + !internal
124132
print("Should not import a scriptPubKey without internal and with public key")
@@ -135,11 +143,11 @@ def run_test (self):
135143
address_assert = self.nodes[1].validateaddress(address['address'])
136144
assert_equal(address_assert['iswatchonly'], False)
137145
assert_equal(address_assert['ismine'], False)
146+
assert_equal('timestamp' in address_assert, False)
138147

139148
# Address + Private key + !watchonly
140149
print("Should import an address with private key")
141150
address = self.nodes[0].validateaddress(self.nodes[0].getnewaddress())
142-
timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime']
143151
result = self.nodes[1].importmulti([{
144152
"scriptPubKey": {
145153
"address": address['address']
@@ -170,6 +178,7 @@ def run_test (self):
170178
address_assert = self.nodes[1].validateaddress(address['address'])
171179
assert_equal(address_assert['iswatchonly'], False)
172180
assert_equal(address_assert['ismine'], False)
181+
assert_equal('timestamp' in address_assert, False)
173182

174183
# ScriptPubKey + Private key + internal
175184
print("Should import a scriptPubKey with internal and with private key")
@@ -184,6 +193,7 @@ def run_test (self):
184193
address_assert = self.nodes[1].validateaddress(address['address'])
185194
assert_equal(address_assert['iswatchonly'], False)
186195
assert_equal(address_assert['ismine'], True)
196+
assert_equal(address_assert['timestamp'], timestamp)
187197

188198
# ScriptPubKey + Private key + !internal
189199
print("Should not import a scriptPubKey without internal and with private key")
@@ -199,6 +209,7 @@ def run_test (self):
199209
address_assert = self.nodes[1].validateaddress(address['address'])
200210
assert_equal(address_assert['iswatchonly'], False)
201211
assert_equal(address_assert['ismine'], False)
212+
assert_equal('timestamp' in address_assert, False)
202213

203214

204215
# P2SH address
@@ -209,6 +220,7 @@ def run_test (self):
209220
self.nodes[1].generate(100)
210221
transactionid = self.nodes[1].sendtoaddress(multi_sig_script['address'], 10.00)
211222
self.nodes[1].generate(1)
223+
timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime']
212224
transaction = self.nodes[1].gettransaction(transactionid)
213225

214226
print("Should import a p2sh")
@@ -222,6 +234,7 @@ def run_test (self):
222234
address_assert = self.nodes[1].validateaddress(multi_sig_script['address'])
223235
assert_equal(address_assert['isscript'], True)
224236
assert_equal(address_assert['iswatchonly'], True)
237+
assert_equal(address_assert['timestamp'], timestamp)
225238
p2shunspent = self.nodes[1].listunspent(0,999999, [multi_sig_script['address']])[0]
226239
assert_equal(p2shunspent['spendable'], False)
227240
assert_equal(p2shunspent['solvable'], False)
@@ -235,6 +248,7 @@ def run_test (self):
235248
self.nodes[1].generate(100)
236249
transactionid = self.nodes[1].sendtoaddress(multi_sig_script['address'], 10.00)
237250
self.nodes[1].generate(1)
251+
timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime']
238252
transaction = self.nodes[1].gettransaction(transactionid)
239253

240254
print("Should import a p2sh with respective redeem script")
@@ -246,6 +260,8 @@ def run_test (self):
246260
"redeemscript": multi_sig_script['redeemScript']
247261
}])
248262
assert_equal(result[0]['success'], True)
263+
address_assert = self.nodes[1].validateaddress(multi_sig_script['address'])
264+
assert_equal(address_assert['timestamp'], timestamp)
249265

250266
p2shunspent = self.nodes[1].listunspent(0,999999, [multi_sig_script['address']])[0]
251267
assert_equal(p2shunspent['spendable'], False)
@@ -260,6 +276,7 @@ def run_test (self):
260276
self.nodes[1].generate(100)
261277
transactionid = self.nodes[1].sendtoaddress(multi_sig_script['address'], 10.00)
262278
self.nodes[1].generate(1)
279+
timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime']
263280
transaction = self.nodes[1].gettransaction(transactionid)
264281

265282
print("Should import a p2sh with respective redeem script and private keys")
@@ -272,6 +289,8 @@ def run_test (self):
272289
"keys": [ self.nodes[0].dumpprivkey(sig_address_1['address']), self.nodes[0].dumpprivkey(sig_address_2['address'])]
273290
}])
274291
assert_equal(result[0]['success'], True)
292+
address_assert = self.nodes[1].validateaddress(multi_sig_script['address'])
293+
assert_equal(address_assert['timestamp'], timestamp)
275294

276295
p2shunspent = self.nodes[1].listunspent(0,999999, [multi_sig_script['address']])[0]
277296
assert_equal(p2shunspent['spendable'], False)
@@ -319,6 +338,7 @@ def run_test (self):
319338
address_assert = self.nodes[1].validateaddress(address['address'])
320339
assert_equal(address_assert['iswatchonly'], False)
321340
assert_equal(address_assert['ismine'], False)
341+
assert_equal('timestamp' in address_assert, False)
322342

323343

324344
# ScriptPubKey + Public key + internal + Wrong pubkey
@@ -338,6 +358,7 @@ def run_test (self):
338358
address_assert = self.nodes[1].validateaddress(address['address'])
339359
assert_equal(address_assert['iswatchonly'], False)
340360
assert_equal(address_assert['ismine'], False)
361+
assert_equal('timestamp' in address_assert, False)
341362

342363

343364
# Address + Private key + !watchonly + Wrong private key
@@ -357,6 +378,7 @@ def run_test (self):
357378
address_assert = self.nodes[1].validateaddress(address['address'])
358379
assert_equal(address_assert['iswatchonly'], False)
359380
assert_equal(address_assert['ismine'], False)
381+
assert_equal('timestamp' in address_assert, False)
360382

361383

362384
# ScriptPubKey + Private key + internal + Wrong private key
@@ -375,6 +397,15 @@ def run_test (self):
375397
address_assert = self.nodes[1].validateaddress(address['address'])
376398
assert_equal(address_assert['iswatchonly'], False)
377399
assert_equal(address_assert['ismine'], False)
400+
assert_equal('timestamp' in address_assert, False)
401+
402+
# restart nodes to check for proper serialization/deserialization of watch only address
403+
stop_nodes(self.nodes)
404+
self.nodes = start_nodes(2, self.options.tmpdir)
405+
address_assert = self.nodes[1].validateaddress(watchonly_address)
406+
assert_equal(address_assert['iswatchonly'], True)
407+
assert_equal(address_assert['ismine'], False)
408+
assert_equal(address_assert['timestamp'], watchonly_timestamp);
378409

379410
# Bad or missing timestamps
380411
print("Should throw on invalid or missing timestamp values")

src/rpc/misc.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ UniValue validateaddress(const JSONRPCRequest& request)
208208
if (pwalletMain) {
209209
const auto& meta = pwalletMain->mapKeyMetadata;
210210
auto it = address.GetKeyID(keyID) ? meta.find(keyID) : meta.end();
211+
if (it == meta.end()) {
212+
it = meta.find(CScriptID(scriptPubKey));
213+
}
211214
if (it != meta.end()) {
212215
ret.push_back(Pair("timestamp", it->second.nCreateTime));
213216
if (!it->second.hdKeypath.empty()) {

src/wallet/rpcdump.cpp

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ void ImportScript(const CScript& script, const string& strLabel, bool isRedeemSc
161161

162162
pwalletMain->MarkDirty();
163163

164-
if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script))
164+
if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script, 0 /* nCreateTime */))
165165
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
166166

167167
if (isRedeemScript) {
@@ -575,15 +575,17 @@ UniValue dumpwallet(const JSONRPCRequest& request)
575575
if (!file.is_open())
576576
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
577577

578-
std::map<CKeyID, int64_t> mapKeyBirth;
578+
std::map<CTxDestination, int64_t> mapKeyBirth;
579579
std::set<CKeyID> setKeyPool;
580580
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
581581
pwalletMain->GetAllReserveKeys(setKeyPool);
582582

583583
// sort time/key pairs
584584
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
585-
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
586-
vKeyBirth.push_back(std::make_pair(it->second, it->first));
585+
for (const auto& entry : mapKeyBirth) {
586+
if (const CKeyID* keyID = boost::get<CKeyID>(&entry.first)) { // set and test
587+
vKeyBirth.push_back(std::make_pair(entry.second, *keyID));
588+
}
587589
}
588590
mapKeyBirth.clear();
589591
std::sort(vKeyBirth.begin(), vKeyBirth.end());
@@ -720,7 +722,7 @@ UniValue ProcessImport(const UniValue& data, const int64_t timestamp)
720722

721723
pwalletMain->MarkDirty();
722724

723-
if (!pwalletMain->HaveWatchOnly(redeemScript) && !pwalletMain->AddWatchOnly(redeemScript)) {
725+
if (!pwalletMain->HaveWatchOnly(redeemScript) && !pwalletMain->AddWatchOnly(redeemScript, timestamp)) {
724726
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
725727
}
726728

@@ -737,7 +739,7 @@ UniValue ProcessImport(const UniValue& data, const int64_t timestamp)
737739

738740
pwalletMain->MarkDirty();
739741

740-
if (!pwalletMain->HaveWatchOnly(redeemDestination) && !pwalletMain->AddWatchOnly(redeemDestination)) {
742+
if (!pwalletMain->HaveWatchOnly(redeemDestination) && !pwalletMain->AddWatchOnly(redeemDestination, timestamp)) {
741743
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
742744
}
743745

@@ -830,7 +832,7 @@ UniValue ProcessImport(const UniValue& data, const int64_t timestamp)
830832

831833
pwalletMain->MarkDirty();
832834

833-
if (!pwalletMain->HaveWatchOnly(pubKeyScript) && !pwalletMain->AddWatchOnly(pubKeyScript)) {
835+
if (!pwalletMain->HaveWatchOnly(pubKeyScript) && !pwalletMain->AddWatchOnly(pubKeyScript, timestamp)) {
834836
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
835837
}
836838

@@ -848,7 +850,7 @@ UniValue ProcessImport(const UniValue& data, const int64_t timestamp)
848850

849851
pwalletMain->MarkDirty();
850852

851-
if (!pwalletMain->HaveWatchOnly(scriptRawPubKey) && !pwalletMain->AddWatchOnly(scriptRawPubKey)) {
853+
if (!pwalletMain->HaveWatchOnly(scriptRawPubKey) && !pwalletMain->AddWatchOnly(scriptRawPubKey, timestamp)) {
852854
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
853855
}
854856

@@ -922,7 +924,7 @@ UniValue ProcessImport(const UniValue& data, const int64_t timestamp)
922924

923925
pwalletMain->MarkDirty();
924926

925-
if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script)) {
927+
if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script, timestamp)) {
926928
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
927929
}
928930

src/wallet/wallet.cpp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,11 @@ bool CWallet::AddCryptedKey(const CPubKey &vchPubKey,
206206
return false;
207207
}
208208

209-
bool CWallet::LoadKeyMetadata(const CPubKey &pubkey, const CKeyMetadata &meta)
209+
bool CWallet::LoadKeyMetadata(const CTxDestination& keyID, const CKeyMetadata &meta)
210210
{
211211
AssertLockHeld(cs_wallet); // mapKeyMetadata
212212
UpdateTimeFirstKey(meta.nCreateTime);
213-
mapKeyMetadata[pubkey.GetID()] = meta;
213+
mapKeyMetadata[keyID] = meta;
214214
return true;
215215
}
216216

@@ -256,15 +256,22 @@ bool CWallet::LoadCScript(const CScript& redeemScript)
256256
return CCryptoKeyStore::AddCScript(redeemScript);
257257
}
258258

259-
bool CWallet::AddWatchOnly(const CScript &dest)
259+
bool CWallet::AddWatchOnly(const CScript& dest)
260260
{
261261
if (!CCryptoKeyStore::AddWatchOnly(dest))
262262
return false;
263-
nTimeFirstKey = 1; // No birthday information for watch-only keys.
263+
const CKeyMetadata& meta = mapKeyMetadata[CScriptID(dest)];
264+
UpdateTimeFirstKey(meta.nCreateTime);
264265
NotifyWatchonlyChanged(true);
265266
if (!fFileBacked)
266267
return true;
267-
return CWalletDB(strWalletFile).WriteWatchOnly(dest);
268+
return CWalletDB(strWalletFile).WriteWatchOnly(dest, meta);
269+
}
270+
271+
bool CWallet::AddWatchOnly(const CScript& dest, int64_t nCreateTime)
272+
{
273+
mapKeyMetadata[CScriptID(dest)].nCreateTime = nCreateTime;
274+
return AddWatchOnly(dest);
268275
}
269276

270277
bool CWallet::RemoveWatchOnly(const CScript &dest)
@@ -3425,14 +3432,16 @@ class CAffectedKeysVisitor : public boost::static_visitor<void> {
34253432
void operator()(const CNoDestination &none) {}
34263433
};
34273434

3428-
void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const {
3435+
void CWallet::GetKeyBirthTimes(std::map<CTxDestination, int64_t> &mapKeyBirth) const {
34293436
AssertLockHeld(cs_wallet); // mapKeyMetadata
34303437
mapKeyBirth.clear();
34313438

34323439
// get birth times for keys with metadata
3433-
for (std::map<CKeyID, CKeyMetadata>::const_iterator it = mapKeyMetadata.begin(); it != mapKeyMetadata.end(); it++)
3434-
if (it->second.nCreateTime)
3435-
mapKeyBirth[it->first] = it->second.nCreateTime;
3440+
for (const auto& entry : mapKeyMetadata) {
3441+
if (entry.second.nCreateTime) {
3442+
mapKeyBirth[entry.first] = entry.second.nCreateTime;
3443+
}
3444+
}
34363445

34373446
// map in which we'll infer heights of other keys
34383447
CBlockIndex *pindexMax = chainActive[std::max(0, chainActive.Height() - 144)]; // the tip can be reorganized; use a 144-block safety margin

src/wallet/wallet.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,17 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
614614

615615
int64_t nTimeFirstKey;
616616

617+
/**
618+
* Private version of AddWatchOnly method which does not accept a
619+
* timestamp, and which will reset the wallet's nTimeFirstKey value to 1 if
620+
* the watch key did not previously have a timestamp associated with it.
621+
* Because this is an inherited virtual method, it is accessible despite
622+
* being marked private, but it is marked private anyway to encourage use
623+
* of the other AddWatchOnly which accepts a timestamp and sets
624+
* nTimeFirstKey more intelligently for more efficient rescans.
625+
*/
626+
bool AddWatchOnly(const CScript& dest) override;
627+
617628
public:
618629
/*
619630
* Main wallet lock.
@@ -638,7 +649,9 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
638649
mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime);
639650
}
640651

641-
std::map<CKeyID, CKeyMetadata> mapKeyMetadata;
652+
// Map from Key ID (for regular keys) or Script ID (for watch-only keys) to
653+
// key metadata.
654+
std::map<CTxDestination, CKeyMetadata> mapKeyMetadata;
642655

643656
typedef std::map<unsigned int, CMasterKey> MasterKeyMap;
644657
MasterKeyMap mapMasterKeys;
@@ -728,7 +741,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
728741
//! Adds a key to the store, without saving it to disk (used by LoadWallet)
729742
bool LoadKey(const CKey& key, const CPubKey &pubkey) { return CCryptoKeyStore::AddKeyPubKey(key, pubkey); }
730743
//! Load metadata (used by LoadWallet)
731-
bool LoadKeyMetadata(const CPubKey &pubkey, const CKeyMetadata &metadata);
744+
bool LoadKeyMetadata(const CTxDestination& pubKey, const CKeyMetadata &metadata);
732745

733746
bool LoadMinVersion(int nVersion) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; nWalletMaxVersion = std::max(nWalletMaxVersion, nVersion); return true; }
734747
void UpdateTimeFirstKey(int64_t nCreateTime);
@@ -750,7 +763,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
750763
bool GetDestData(const CTxDestination &dest, const std::string &key, std::string *value) const;
751764

752765
//! Adds a watch-only address to the store, and saves it to disk.
753-
bool AddWatchOnly(const CScript &dest);
766+
bool AddWatchOnly(const CScript& dest, int64_t nCreateTime);
754767
bool RemoveWatchOnly(const CScript &dest);
755768
//! Adds a watch-only address to the store, without saving it to disk (used by LoadWallet)
756769
bool LoadWatchOnly(const CScript &dest);
@@ -759,7 +772,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
759772
bool ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase);
760773
bool EncryptWallet(const SecureString& strWalletPassphrase);
761774

762-
void GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const;
775+
void GetKeyBirthTimes(std::map<CTxDestination, int64_t> &mapKeyBirth) const;
763776

764777
/**
765778
* Increment the next transaction order id

0 commit comments

Comments
 (0)