Skip to content

Commit 08e6aaa

Browse files
committed
Merge bitcoin#28920: wallet: birth time update during tx scanning
1ce45ba rpc: getwalletinfo, return wallet 'birthtime' (furszy) 83c6644 test: coverage for wallet birth time interaction with -reindex (furszy) 6f49737 wallet: fix legacy spkm default birth time (furszy) 75fbf44 wallet: birth time update during tx scanning (furszy) b4306e3 refactor: rename FirstKeyTimeChanged to MaybeUpdateBirthTime (furszy) Pull request description: Fixing bitcoin#28897. As the user may have imported a descriptor with a timestamp newer than the actual birth time of the first key (by setting 'timestamp=now'), the wallet needs to update the birth time when it detects a transaction older than the oldest descriptor timestamp. Testing Notes: Can cherry-pick the test commit on top of master. It will fail there. ACKs for top commit: Sjors: re-utACK 1ce45ba achow101: ACK 1ce45ba Tree-SHA512: 10c2382f87356ae9ea3fcb637d7edc5ed0e51e13cc2729c314c9ffb57c684b9ac3c4b757b85810c0a674020b7287c43d3be8273bcf75e2aff0cc1c037f1159f9
2 parents 4ad5c71 + 1ce45ba commit 08e6aaa

File tree

7 files changed

+140
-10
lines changed

7 files changed

+140
-10
lines changed

src/wallet/rpc/wallet.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ static RPCHelpMan getwalletinfo()
6969
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
7070
{RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"},
7171
{RPCResult::Type::BOOL, "blank", "Whether this wallet intentionally does not contain any keys, scripts, or descriptors"},
72+
{RPCResult::Type::NUM_TIME, "birthtime", /*optional=*/true, "The start time for blocks scanning. It could be modified by (re)importing any descriptor with an earlier timestamp."},
7273
RESULT_LAST_PROCESSED_BLOCK,
7374
}},
7475
},
@@ -132,6 +133,9 @@ static RPCHelpMan getwalletinfo()
132133
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
133134
obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
134135
obj.pushKV("blank", pwallet->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET));
136+
if (int64_t birthtime = pwallet->GetBirthTime(); birthtime != UNKNOWN_TIME) {
137+
obj.pushKV("birthtime", birthtime);
138+
}
135139

136140
AppendLastProcessedBlock(obj, *pwallet);
137141
return obj;

src/wallet/scriptpubkeyman.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ void LegacyScriptPubKeyMan::UpdateTimeFirstKey(int64_t nCreateTime)
710710
// Cannot determine birthday information, so set the wallet birthday to
711711
// the beginning of time.
712712
nTimeFirstKey = 1;
713-
} else if (!nTimeFirstKey || nCreateTime < nTimeFirstKey) {
713+
} else if (nTimeFirstKey == UNKNOWN_TIME || nCreateTime < nTimeFirstKey) {
714714
nTimeFirstKey = nCreateTime;
715715
}
716716

src/wallet/scriptpubkeyman.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ class WalletStorage
5151
virtual bool IsLocked() const = 0;
5252
};
5353

54+
//! Constant representing an unknown spkm creation time
55+
static constexpr int64_t UNKNOWN_TIME = std::numeric_limits<int64_t>::max();
56+
5457
//! Default for -keypool
5558
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
5659

@@ -286,7 +289,8 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
286289
WatchOnlySet setWatchOnly GUARDED_BY(cs_KeyStore);
287290
WatchKeyMap mapWatchKeys GUARDED_BY(cs_KeyStore);
288291

289-
int64_t nTimeFirstKey GUARDED_BY(cs_KeyStore) = 0;
292+
// By default, do not scan any block until keys/scripts are generated/imported
293+
int64_t nTimeFirstKey GUARDED_BY(cs_KeyStore) = UNKNOWN_TIME;
290294

291295
//! Number of pre-generated keys/scripts (part of the look-ahead process, used to detect payments)
292296
int64_t m_keypool_size GUARDED_BY(cs_KeyStore){DEFAULT_KEYPOOL_SIZE};

src/wallet/wallet.cpp

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,9 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
10801080
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
10811081
wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block);
10821082
AddToSpends(wtx, &batch);
1083+
1084+
// Update birth time when tx time is older than it.
1085+
MaybeUpdateBirthTime(wtx.GetTxTime());
10831086
}
10841087

10851088
if (!fInsertedNew)
@@ -1199,6 +1202,10 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
11991202
}
12001203
}
12011204
}
1205+
1206+
// Update birth time when tx time is older than it.
1207+
MaybeUpdateBirthTime(wtx.GetTxTime());
1208+
12021209
return true;
12031210
}
12041211

@@ -1747,11 +1754,11 @@ bool CWallet::ImportScriptPubKeys(const std::string& label, const std::set<CScri
17471754
return true;
17481755
}
17491756

1750-
void CWallet::FirstKeyTimeChanged(const ScriptPubKeyMan* spkm, int64_t new_birth_time)
1757+
void CWallet::MaybeUpdateBirthTime(int64_t time)
17511758
{
17521759
int64_t birthtime = m_birth_time.load();
1753-
if (new_birth_time < birthtime) {
1754-
m_birth_time = new_birth_time;
1760+
if (time < birthtime) {
1761+
m_birth_time = time;
17551762
}
17561763
}
17571764

@@ -3089,7 +3096,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
30893096
int64_t time = spk_man->GetTimeFirstKey();
30903097
if (!time_first_key || time < *time_first_key) time_first_key = time;
30913098
}
3092-
if (time_first_key) walletInstance->m_birth_time = *time_first_key;
3099+
if (time_first_key) walletInstance->MaybeUpdateBirthTime(*time_first_key);
30933100

30943101
if (chain && !AttachChain(walletInstance, *chain, rescan_required, error, warnings)) {
30953102
return nullptr;
@@ -3482,10 +3489,12 @@ LegacyScriptPubKeyMan* CWallet::GetOrCreateLegacyScriptPubKeyMan()
34823489

34833490
void CWallet::AddScriptPubKeyMan(const uint256& id, std::unique_ptr<ScriptPubKeyMan> spkm_man)
34843491
{
3492+
// Add spkm_man to m_spk_managers before calling any method
3493+
// that might access it.
34853494
const auto& spkm = m_spk_managers[id] = std::move(spkm_man);
34863495

34873496
// Update birth time if needed
3488-
FirstKeyTimeChanged(spkm.get(), spkm->GetTimeFirstKey());
3497+
MaybeUpdateBirthTime(spkm->GetTimeFirstKey());
34893498
}
34903499

34913500
void CWallet::SetupLegacyScriptPubKeyMan()
@@ -3518,7 +3527,7 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
35183527
for (const auto& spk_man : GetActiveScriptPubKeyMans()) {
35193528
spk_man->NotifyWatchonlyChanged.connect(NotifyWatchonlyChanged);
35203529
spk_man->NotifyCanGetAddressesChanged.connect(NotifyCanGetAddressesChanged);
3521-
spk_man->NotifyFirstKeyTimeChanged.connect(std::bind(&CWallet::FirstKeyTimeChanged, this, std::placeholders::_1, std::placeholders::_2));
3530+
spk_man->NotifyFirstKeyTimeChanged.connect(std::bind(&CWallet::MaybeUpdateBirthTime, this, std::placeholders::_2));
35223531
}
35233532
}
35243533

src/wallet/wallet.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
688688
bool ImportPubKeys(const std::vector<CKeyID>& ordered_pubkeys, const std::map<CKeyID, CPubKey>& pubkey_map, const std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
689689
bool ImportScriptPubKeys(const std::string& label, const std::set<CScript>& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
690690

691-
/** Updates wallet birth time if 'new_birth_time' is below it */
692-
void FirstKeyTimeChanged(const ScriptPubKeyMan* spkm, int64_t new_birth_time);
691+
/** Updates wallet birth time if 'time' is below it */
692+
void MaybeUpdateBirthTime(int64_t time);
693693

694694
CFeeRate m_pay_tx_fee{DEFAULT_PAY_TX_FEE};
695695
unsigned int m_confirm_target{DEFAULT_TX_CONFIRM_TARGET};
@@ -887,6 +887,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
887887
/* Returns true if the wallet can give out new addresses. This means it has keys in the keypool or can generate new keys */
888888
bool CanGetAddresses(bool internal = false) const;
889889

890+
/* Returns the time of the first created key or, in case of an import, it could be the time of the first received transaction */
891+
int64_t GetBirthTime() const { return m_birth_time; }
892+
890893
/**
891894
* Blocks until the wallet state is up-to-date to /at least/ the current
892895
* chain at the time this function is entered

test/functional/test_runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@
207207
'wallet_createwallet.py --descriptors',
208208
'wallet_watchonly.py --legacy-wallet',
209209
'wallet_watchonly.py --usecli --legacy-wallet',
210+
'wallet_reindex.py --legacy-wallet',
211+
'wallet_reindex.py --descriptors',
210212
'wallet_reorgsrestore.py',
211213
'wallet_conflicts.py --legacy-wallet',
212214
'wallet_conflicts.py --descriptors',

test/functional/wallet_reindex.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2023-present 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+
6+
"""Test wallet-reindex interaction"""
7+
8+
import time
9+
10+
from test_framework.blocktools import COINBASE_MATURITY
11+
from test_framework.test_framework import BitcoinTestFramework
12+
from test_framework.util import (
13+
assert_equal,
14+
)
15+
BLOCK_TIME = 60 * 10
16+
17+
class WalletReindexTest(BitcoinTestFramework):
18+
def add_options(self, parser):
19+
self.add_wallet_options(parser)
20+
21+
def set_test_params(self):
22+
self.num_nodes = 1
23+
self.setup_clean_chain = True
24+
25+
def skip_test_if_missing_module(self):
26+
self.skip_if_no_wallet()
27+
28+
def advance_time(self, node, secs):
29+
self.node_time += secs
30+
node.setmocktime(self.node_time)
31+
32+
# Verify the wallet updates the birth time accordingly when it detects a transaction
33+
# with a time older than the oldest descriptor timestamp.
34+
# This could happen when the user blindly imports a descriptor with 'timestamp=now'.
35+
def birthtime_test(self, node, miner_wallet):
36+
self.log.info("Test birth time update during tx scanning")
37+
# Fund address to test
38+
wallet_addr = miner_wallet.getnewaddress()
39+
tx_id = miner_wallet.sendtoaddress(wallet_addr, 2)
40+
41+
# Generate 50 blocks, one every 10 min to surpass the 2 hours rescan window the wallet has
42+
for _ in range(50):
43+
self.generate(node, 1)
44+
self.advance_time(node, BLOCK_TIME)
45+
46+
# Now create a new wallet, and import the descriptor
47+
node.createwallet(wallet_name='watch_only', disable_private_keys=True, load_on_startup=True)
48+
wallet_watch_only = node.get_wallet_rpc('watch_only')
49+
# Blank wallets don't have a birth time
50+
assert 'birthtime' not in wallet_watch_only.getwalletinfo()
51+
52+
# For a descriptors wallet: Import address with timestamp=now.
53+
# For legacy wallet: There is no way of importing a script/address with a custom time. The wallet always imports it with birthtime=1.
54+
# In both cases, disable rescan to not detect the transaction.
55+
wallet_watch_only.importaddress(wallet_addr, rescan=False)
56+
assert_equal(len(wallet_watch_only.listtransactions()), 0)
57+
58+
# Depending on the wallet type, the birth time changes.
59+
wallet_birthtime = wallet_watch_only.getwalletinfo()['birthtime']
60+
if self.options.descriptors:
61+
# As blocks were generated every 10 min, the chain MTP timestamp is node_time - 60 min.
62+
assert_equal(self.node_time - BLOCK_TIME * 6, wallet_birthtime)
63+
else:
64+
# No way of importing scripts/addresses with a custom time on a legacy wallet.
65+
# It's always set to the beginning of time.
66+
assert_equal(wallet_birthtime, 1)
67+
68+
# Rescan the wallet to detect the missing transaction
69+
wallet_watch_only.rescanblockchain()
70+
assert_equal(wallet_watch_only.gettransaction(tx_id)['confirmations'], 50)
71+
assert_equal(wallet_watch_only.getbalances()['mine' if self.options.descriptors else 'watchonly']['trusted'], 2)
72+
73+
# Reindex and wait for it to finish
74+
with node.assert_debug_log(expected_msgs=["initload thread exit"]):
75+
self.restart_node(0, extra_args=['-reindex=1', f'-mocktime={self.node_time}'])
76+
node.syncwithvalidationinterfacequeue()
77+
78+
# Verify the transaction is still 'confirmed' after reindex
79+
wallet_watch_only = node.get_wallet_rpc('watch_only')
80+
tx_info = wallet_watch_only.gettransaction(tx_id)
81+
assert_equal(tx_info['confirmations'], 50)
82+
83+
# Depending on the wallet type, the birth time changes.
84+
if self.options.descriptors:
85+
# For descriptors, verify the wallet updated the birth time to the transaction time
86+
assert_equal(tx_info['time'], wallet_watch_only.getwalletinfo()['birthtime'])
87+
else:
88+
# For legacy, as the birth time was set to the beginning of time, verify it did not change
89+
assert_equal(wallet_birthtime, 1)
90+
91+
wallet_watch_only.unloadwallet()
92+
93+
def run_test(self):
94+
node = self.nodes[0]
95+
self.node_time = int(time.time())
96+
node.setmocktime(self.node_time)
97+
98+
# Fund miner
99+
node.createwallet(wallet_name='miner', load_on_startup=True)
100+
miner_wallet = node.get_wallet_rpc('miner')
101+
self.generatetoaddress(node, COINBASE_MATURITY + 10, miner_wallet.getnewaddress())
102+
103+
# Tests
104+
self.birthtime_test(node, miner_wallet)
105+
106+
107+
if __name__ == '__main__':
108+
WalletReindexTest().main()

0 commit comments

Comments
 (0)