Skip to content

Commit 33f8f8a

Browse files
committed
Merge bitcoin/bitcoin#30221: wallet: Ensure best block matches wallet scan state
30a94b1 test, wallet: Remove concurrent writes test (Ava Chow) b44b7c0 wallet: Write best block record on unload (Ava Chow) 876a258 wallet: Remove unnecessary database Close step on shutdown (Ava Chow) 98a1a52 wallet: Remove chainStateFlushed (Ava Chow) 7fd3e1c wallet, bench: Write a bestblock record in WalletMigration (Ava Chow) 6d3a8b1 wallet: Replace chainStateFlushed in loading with SetLastBlockProcessed (Ava Chow) 7bacabb wallet: Update best block record after block dis/connect (Ava Chow) Pull request description: Implements the idea discussed in bitcoin/bitcoin#29652 (comment) Currently, `m_last_block_processed` and `m_last_block_processed_height` are not guaranteed to match the block locator stored in the wallet, nor do either of those fields actually represent the last block that the wallet is synced up to. This is confusing and unintuitive. This PR changes those last block fields to be updated whenever the wallet makes a change to the db for new transaction state found in new blocks. Whenever a block is received that contains a transaction relevant to the wallet, the last block locator will now be written to disk. Furthermore, every block disconnection will now write an updated locator. To ensure that the locator is relatively recent and loading rescans are fairly quick in the event of unplanned shutdown, it is also now written every 144 blocks (~1 day). Additionally it is now written when the wallet is unloaded so that it is accurate when the wallet is loaded again. Lastly, the `chainstateFlushed` notification in the wallet is changed to be a no-op. The best block locator record is no longer written when `chainstateFlushed` is received from the node since it should already be mostly up to date. ACKs for top commit: rkrux: ACK 30a94b1 mzumsande: Code Review ACK 30a94b1 ryanofsky: Code review ACK 30a94b1. Only changes since last review are using WriteBestBlock method more places and updating comments. Tree-SHA512: 46117541f8aaf13dde57430e813b4bbbd5e146e2632769675803c8e65a82f149a7cc6026489a127d32684b90124bd2b7c28216dbcfa6a47447300e8f3814e029
2 parents 7054d24 + 30a94b1 commit 33f8f8a

File tree

9 files changed

+82
-112
lines changed

9 files changed

+82
-112
lines changed

src/bench/wallet_migration.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ static void WalletMigration(benchmark::Bench& bench)
2828

2929
// Setup legacy wallet
3030
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(test_setup->m_node.chain.get(), "", CreateMockableWalletDatabase());
31-
wallet->chainStateFlushed(ChainstateRole::NORMAL, CBlockLocator{});
3231
LegacyDataSPKM* legacy_spkm = wallet->GetOrCreateLegacyDataSPKM();
3332
WalletBatch batch{wallet->GetDatabase()};
3433

34+
// Write a best block record as migration expects one to exist
35+
CBlockLocator loc;
36+
batch.WriteBestBlock(loc);
37+
3538
// Add watch-only addresses
3639
std::vector<CScript> scripts_watch_only;
3740
for (int w = 0; w < NUM_WATCH_ONLY_ADDR; ++w) {

src/wallet/interfaces.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ class WalletLoaderImpl : public WalletLoader
553553
m_context.chain = &chain;
554554
m_context.args = &args;
555555
}
556-
~WalletLoaderImpl() override { UnloadWallets(m_context); }
556+
~WalletLoaderImpl() override { stop(); }
557557

558558
//! ChainClient methods
559559
void registerRpcs() override
@@ -574,7 +574,7 @@ class WalletLoaderImpl : public WalletLoader
574574
m_context.scheduler = &scheduler;
575575
return StartWallets(m_context);
576576
}
577-
void stop() override { return StopWallets(m_context); }
577+
void stop() override { return UnloadWallets(m_context); }
578578
void setMockTime(int64_t time) override { return SetMockTime(time); }
579579
void schedulerMockForward(std::chrono::seconds delta) override { Assert(m_context.scheduler)->MockForward(delta); }
580580

src/wallet/load.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,6 @@ void StartWallets(WalletContext& context)
162162
context.scheduler->scheduleEvery([&context] { MaybeResendWalletTxs(context); }, 1min);
163163
}
164164

165-
void StopWallets(WalletContext& context)
166-
{
167-
for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) {
168-
pwallet->Close();
169-
}
170-
}
171-
172165
void UnloadWallets(WalletContext& context)
173166
{
174167
auto wallets = GetWallets(context);

src/wallet/load.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ bool LoadWallets(WalletContext& context);
2828
//! Complete startup of wallets.
2929
void StartWallets(WalletContext& context);
3030

31-
//! Stop all wallets. Wallets will be flushed first.
32-
void StopWallets(WalletContext& context);
33-
34-
//! Close all wallets.
3531
void UnloadWallets(WalletContext& context);
3632
} // namespace wallet
3733

src/wallet/test/wallet_tests.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
106106
LOCK(wallet.cs_wallet);
107107
LOCK(Assert(m_node.chainman)->GetMutex());
108108
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
109-
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
109+
wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash());
110110
}
111111
AddKey(wallet, coinbaseKey);
112112
WalletRescanReserver reserver(wallet);
@@ -116,8 +116,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
116116

117117
{
118118
CBlockLocator locator;
119-
BOOST_CHECK(!WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
120-
BOOST_CHECK(locator.IsNull());
119+
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
120+
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
121121
}
122122

123123
CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/true);
@@ -130,7 +130,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
130130
{
131131
CBlockLocator locator;
132132
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
133-
BOOST_CHECK(!locator.IsNull());
133+
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
134134
}
135135
}
136136

src/wallet/wallet.cpp

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ bool RemoveWallet(WalletContext& context, const std::shared_ptr<CWallet>& wallet
164164

165165
interfaces::Chain& chain = wallet->chain();
166166
std::string name = wallet->GetName();
167+
WITH_LOCK(wallet->cs_wallet, wallet->WriteBestBlock());
167168

168169
// Unregister with the validation interface which also drops shared pointers.
169170
wallet->m_chain_notifications_handler.reset();
@@ -652,15 +653,20 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
652653
return false;
653654
}
654655

655-
void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc)
656+
void CWallet::SetLastBlockProcessedInMem(int block_height, uint256 block_hash)
656657
{
657-
// Don't update the best block until the chain is attached so that in case of a shutdown,
658-
// the rescan will be restarted at next startup.
659-
if (m_attaching_chain || role == ChainstateRole::BACKGROUND) {
660-
return;
661-
}
662-
WalletBatch batch(GetDatabase());
663-
batch.WriteBestBlock(loc);
658+
AssertLockHeld(cs_wallet);
659+
660+
m_last_block_processed = block_hash;
661+
m_last_block_processed_height = block_height;
662+
}
663+
664+
void CWallet::SetLastBlockProcessed(int block_height, uint256 block_hash)
665+
{
666+
AssertLockHeld(cs_wallet);
667+
668+
SetLastBlockProcessedInMem(block_height, block_hash);
669+
WriteBestBlock();
664670
}
665671

666672
void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in)
@@ -1377,15 +1383,16 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const Txid& tx_hash, co
13771383
}
13781384
}
13791385

1380-
void CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
1386+
bool CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
13811387
{
13821388
if (!AddToWalletIfInvolvingMe(ptx, state, update_tx, rescanning_old_block))
1383-
return; // Not one of ours
1389+
return false; // Not one of ours
13841390

13851391
// If a transaction changes 'conflicted' state, that changes the balance
13861392
// available of the outputs it spends. So force those to be
13871393
// recomputed, also:
13881394
MarkInputsDirty(ptx);
1395+
return true;
13891396
}
13901397

13911398
void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
@@ -1472,18 +1479,25 @@ void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& b
14721479
assert(block.data);
14731480
LOCK(cs_wallet);
14741481

1475-
m_last_block_processed_height = block.height;
1476-
m_last_block_processed = block.hash;
1482+
// Update the best block in memory first. This will set the best block's height, which is
1483+
// needed by MarkConflicted.
1484+
SetLastBlockProcessedInMem(block.height, block.hash);
14771485

14781486
// No need to scan block if it was created before the wallet birthday.
14791487
// Uses chain max time and twice the grace period to adjust time for block time variability.
14801488
if (block.chain_time_max < m_birth_time.load() - (TIMESTAMP_WINDOW * 2)) return;
14811489

14821490
// Scan block
1491+
bool wallet_updated = false;
14831492
for (size_t index = 0; index < block.data->vtx.size(); index++) {
1484-
SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
1493+
wallet_updated |= SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
14851494
transactionRemovedFromMempool(block.data->vtx[index], MemPoolRemovalReason::BLOCK);
14861495
}
1496+
1497+
// Update on disk if this block resulted in us updating a tx, or periodically every 144 blocks (~1 day)
1498+
if (wallet_updated || block.height % 144 == 0) {
1499+
WriteBestBlock();
1500+
}
14871501
}
14881502

14891503
void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
@@ -1495,9 +1509,6 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
14951509
// be unconfirmed, whether or not the transaction is added back to the mempool.
14961510
// User may have to call abandontransaction again. It may be addressed in the
14971511
// future with a stickier abandoned state or even removing abandontransaction call.
1498-
m_last_block_processed_height = block.height - 1;
1499-
m_last_block_processed = *Assert(block.prev_hash);
1500-
15011512
int disconnect_height = block.height;
15021513

15031514
for (size_t index = 0; index < block.data->vtx.size(); index++) {
@@ -1531,6 +1542,9 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
15311542
}
15321543
}
15331544
}
1545+
1546+
// Update the best block
1547+
SetLastBlockProcessed(block.height - 1, *Assert(block.prev_hash));
15341548
}
15351549

15361550
void CWallet::updatedBlockTip()
@@ -2885,6 +2899,8 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
28852899
!walletInstance->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET);
28862900
if (fFirstRun)
28872901
{
2902+
LOCK(walletInstance->cs_wallet);
2903+
28882904
// ensure this wallet.dat can only be opened by clients supporting HD with chain split and expects no default key
28892905
walletInstance->SetMinVersion(FEATURE_LATEST);
28902906

@@ -2894,7 +2910,6 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
28942910
assert(walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
28952911

28962912
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) || !(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
2897-
LOCK(walletInstance->cs_wallet);
28982913
if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
28992914
walletInstance->SetupDescriptorScriptPubKeyMans();
29002915
// SetupDescriptorScriptPubKeyMans already calls SetupGeneration for us so we don't need to call SetupGeneration separately
@@ -2910,7 +2925,10 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
29102925
}
29112926

29122927
if (chain) {
2913-
walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain->getTipLocator());
2928+
std::optional<int> tip_height = chain->getHeight();
2929+
if (tip_height) {
2930+
walletInstance->SetLastBlockProcessed(*tip_height, chain->getBlockHash(*tip_height));
2931+
}
29142932
}
29152933
} else if (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS) {
29162934
// Make it impossible to disable private keys after creation
@@ -3112,11 +3130,6 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
31123130
// be pending on the validation-side until lock release. Blocks that are connected while the
31133131
// rescan is ongoing will not be processed in the rescan but with the block connected notifications,
31143132
// so the wallet will only be completeley synced after the notifications delivery.
3115-
// chainStateFlushed notifications are ignored until the rescan is finished
3116-
// so that in case of a shutdown event, the rescan will be repeated at the next start.
3117-
// This is temporary until rescan and notifications delivery are unified under same
3118-
// interface.
3119-
walletInstance->m_attaching_chain = true; //ignores chainStateFlushed notifications
31203133
walletInstance->m_chain_notifications_handler = walletInstance->chain().handleNotifications(walletInstance);
31213134

31223135
// If rescan_required = true, rescan_height remains equal to 0
@@ -3193,15 +3206,21 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
31933206

31943207
{
31953208
WalletRescanReserver reserver(*walletInstance);
3196-
if (!reserver.reserve() || (ScanResult::SUCCESS != walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/true).status)) {
3209+
if (!reserver.reserve()) {
3210+
error = _("Failed to acquire rescan reserver during wallet initialization");
3211+
return false;
3212+
}
3213+
ScanResult scan_res = walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/true);
3214+
if (ScanResult::SUCCESS != scan_res.status) {
31973215
error = _("Failed to rescan the wallet during initialization");
31983216
return false;
31993217
}
3218+
// Set and update the best block record
3219+
// Set last block scanned as the last block processed as it may be different in case the case of a reorg.
3220+
// Also save the best block locator because rescanning only updates it intermittently.
3221+
walletInstance->SetLastBlockProcessed(*scan_res.last_scanned_height, scan_res.last_scanned_block);
32003222
}
3201-
walletInstance->m_attaching_chain = false;
3202-
walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain.getTipLocator());
32033223
}
3204-
walletInstance->m_attaching_chain = false;
32053224

32063225
return true;
32073226
}
@@ -3261,14 +3280,7 @@ void CWallet::postInitProcess()
32613280

32623281
bool CWallet::BackupWallet(const std::string& strDest) const
32633282
{
3264-
if (m_chain) {
3265-
CBlockLocator loc;
3266-
WITH_LOCK(cs_wallet, chain().findBlock(m_last_block_processed, FoundBlock().locator(loc)));
3267-
if (!loc.IsNull()) {
3268-
WalletBatch batch(GetDatabase());
3269-
batch.WriteBestBlock(loc);
3270-
}
3271-
}
3283+
WITH_LOCK(cs_wallet, WriteBestBlock());
32723284
return GetDatabase().Backup(strDest);
32733285
}
32743286

@@ -4197,11 +4209,6 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
41974209
return util::Error{_("Error: This wallet is already a descriptor wallet")};
41984210
}
41994211

4200-
// Flush chain state before unloading wallet
4201-
CBlockLocator locator;
4202-
WITH_LOCK(wallet->cs_wallet, context.chain->findBlock(wallet->GetLastBlockHash(), FoundBlock().locator(locator)));
4203-
if (!locator.IsNull()) wallet->chainStateFlushed(ChainstateRole::NORMAL, locator);
4204-
42054212
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
42064213
return util::Error{_("Unable to unload the wallet before migrating")};
42074214
}
@@ -4450,4 +4457,17 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
44504457
}
44514458
return std::nullopt;
44524459
}
4460+
4461+
void CWallet::WriteBestBlock() const
4462+
{
4463+
AssertLockHeld(cs_wallet);
4464+
4465+
if (!m_last_block_processed.IsNull()) {
4466+
CBlockLocator loc;
4467+
chain().findBlock(m_last_block_processed, FoundBlock().locator(loc));
4468+
4469+
WalletBatch batch(GetDatabase());
4470+
batch.WriteBestBlock(loc);
4471+
}
4472+
}
44534473
} // namespace wallet

src/wallet/wallet.h

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
306306

307307
std::atomic<bool> fAbortRescan{false};
308308
std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
309-
std::atomic<bool> m_attaching_chain{false};
310309
std::atomic<bool> m_scanning_with_passphrase{false};
311310
std::atomic<SteadyClock::time_point> m_scanning_start{SteadyClock::time_point{}};
312311
std::atomic<double> m_scanning_progress{0};
@@ -371,7 +370,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
371370

372371
void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
373372

374-
void SyncTransaction(const CTransactionRef& tx, const SyncTxState& state, bool update_tx = true, bool rescanning_old_block = false) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
373+
bool SyncTransaction(const CTransactionRef& tx, const SyncTxState& state, bool update_tx = true, bool rescanning_old_block = false) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
375374

376375
/** WalletFlags set on this wallet. */
377376
std::atomic<uint64_t> m_wallet_flags{0};
@@ -438,6 +437,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
438437

439438
static NodeClock::time_point GetDefaultNextResend();
440439

440+
// Update last block processed in memory only
441+
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
442+
441443
public:
442444
/**
443445
* Main wallet lock.
@@ -780,7 +782,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
780782
/** should probably be renamed to IsRelevantToMe */
781783
bool IsFromMe(const CTransaction& tx) const;
782784
CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const;
783-
void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override;
784785

785786
DBErrors LoadWallet();
786787

@@ -970,13 +971,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
970971
assert(m_last_block_processed_height >= 0);
971972
return m_last_block_processed;
972973
}
973-
/** Set last block processed height, currently only use in unit test */
974-
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
975-
{
976-
AssertLockHeld(cs_wallet);
977-
m_last_block_processed_height = block_height;
978-
m_last_block_processed = block_hash;
979-
};
974+
/** Set last block processed height, and write to database */
975+
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
976+
/** Write the current best block to database */
977+
void WriteBestBlock() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
980978

981979
//! Connect the signals from ScriptPubKeyMans to the signals in CWallet
982980
void ConnectScriptPubKeyManNotifiers();

0 commit comments

Comments
 (0)