Skip to content

Commit 7bacabb

Browse files
committed
wallet: Update best block record after block dis/connect
When a block is connected, if the new block had anything relevant to the wallet, update the best block record on disk. If not, also sync the best block record to disk every 144 blocks. Also reuse the new WriteBestBlock method in BackupWallet.
1 parent 9a05b45 commit 7bacabb

File tree

4 files changed

+62
-32
lines changed

4 files changed

+62
-32
lines changed

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: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,22 @@ void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc)
663663
batch.WriteBestBlock(loc);
664664
}
665665

666+
void CWallet::SetLastBlockProcessedInMem(int block_height, uint256 block_hash)
667+
{
668+
AssertLockHeld(cs_wallet);
669+
670+
m_last_block_processed = block_hash;
671+
m_last_block_processed_height = block_height;
672+
}
673+
674+
void CWallet::SetLastBlockProcessed(int block_height, uint256 block_hash)
675+
{
676+
AssertLockHeld(cs_wallet);
677+
678+
SetLastBlockProcessedInMem(block_height, block_hash);
679+
WriteBestBlock();
680+
}
681+
666682
void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in)
667683
{
668684
LOCK(cs_wallet);
@@ -1378,15 +1394,16 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
13781394
}
13791395
}
13801396

1381-
void CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
1397+
bool CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
13821398
{
13831399
if (!AddToWalletIfInvolvingMe(ptx, state, update_tx, rescanning_old_block))
1384-
return; // Not one of ours
1400+
return false; // Not one of ours
13851401

13861402
// If a transaction changes 'conflicted' state, that changes the balance
13871403
// available of the outputs it spends. So force those to be
13881404
// recomputed, also:
13891405
MarkInputsDirty(ptx);
1406+
return true;
13901407
}
13911408

13921409
void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
@@ -1473,18 +1490,25 @@ void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& b
14731490
assert(block.data);
14741491
LOCK(cs_wallet);
14751492

1476-
m_last_block_processed_height = block.height;
1477-
m_last_block_processed = block.hash;
1493+
// Update the best block in memory first. This will set the best block's height, which is
1494+
// needed by MarkConflicted.
1495+
SetLastBlockProcessedInMem(block.height, block.hash);
14781496

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

14831501
// Scan block
1502+
bool wallet_updated = false;
14841503
for (size_t index = 0; index < block.data->vtx.size(); index++) {
1485-
SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
1504+
wallet_updated |= SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
14861505
transactionRemovedFromMempool(block.data->vtx[index], MemPoolRemovalReason::BLOCK);
14871506
}
1507+
1508+
// Update on disk if this block resulted in us updating a tx, or periodically every 144 blocks (~1 day)
1509+
if (wallet_updated || block.height % 144 == 0) {
1510+
WriteBestBlock();
1511+
}
14881512
}
14891513

14901514
void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
@@ -1496,9 +1520,6 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
14961520
// be unconfirmed, whether or not the transaction is added back to the mempool.
14971521
// User may have to call abandontransaction again. It may be addressed in the
14981522
// future with a stickier abandoned state or even removing abandontransaction call.
1499-
m_last_block_processed_height = block.height - 1;
1500-
m_last_block_processed = *Assert(block.prev_hash);
1501-
15021523
int disconnect_height = block.height;
15031524

15041525
for (size_t index = 0; index < block.data->vtx.size(); index++) {
@@ -1532,6 +1553,9 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
15321553
}
15331554
}
15341555
}
1556+
1557+
// Update the best block
1558+
SetLastBlockProcessed(block.height - 1, *Assert(block.prev_hash));
15351559
}
15361560

15371561
void CWallet::updatedBlockTip()
@@ -3264,14 +3288,7 @@ void CWallet::postInitProcess()
32643288

32653289
bool CWallet::BackupWallet(const std::string& strDest) const
32663290
{
3267-
if (m_chain) {
3268-
CBlockLocator loc;
3269-
WITH_LOCK(cs_wallet, chain().findBlock(m_last_block_processed, FoundBlock().locator(loc)));
3270-
if (!loc.IsNull()) {
3271-
WalletBatch batch(GetDatabase());
3272-
batch.WriteBestBlock(loc);
3273-
}
3274-
}
3291+
WITH_LOCK(cs_wallet, WriteBestBlock());
32753292
return GetDatabase().Backup(strDest);
32763293
}
32773294

@@ -4453,4 +4470,17 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
44534470
}
44544471
return std::nullopt;
44554472
}
4473+
4474+
void CWallet::WriteBestBlock() const
4475+
{
4476+
AssertLockHeld(cs_wallet);
4477+
4478+
if (!m_last_block_processed.IsNull()) {
4479+
CBlockLocator loc;
4480+
chain().findBlock(m_last_block_processed, FoundBlock().locator(loc));
4481+
4482+
WalletBatch batch(GetDatabase());
4483+
batch.WriteBestBlock(loc);
4484+
}
4485+
}
44564486
} // namespace wallet

src/wallet/wallet.h

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

371371
void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
372372

373-
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);
374374

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

438438
static NodeClock::time_point GetDefaultNextResend();
439439

440+
// Update last block processed in memory only
441+
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
442+
440443
public:
441444
/**
442445
* Main wallet lock.
@@ -974,13 +977,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
974977
assert(m_last_block_processed_height >= 0);
975978
return m_last_block_processed;
976979
}
977-
/** Set last block processed height, currently only use in unit test */
978-
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
979-
{
980-
AssertLockHeld(cs_wallet);
981-
m_last_block_processed_height = block_height;
982-
m_last_block_processed = block_hash;
983-
};
980+
/** Set last block processed height, and write to database */
981+
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
982+
/** Write the current best block to database */
983+
void WriteBestBlock() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
984984

985985
//! Connect the signals from ScriptPubKeyMans to the signals in CWallet
986986
void ConnectScriptPubKeyManNotifiers();

test/functional/wallet_reorgsrestore.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ def test_reorg_handling_during_unclean_shutdown(self):
119119
self.start_node(0)
120120
assert_equal(node.getbestblockhash(), tip)
121121

122-
# Due to an existing bug, the wallet incorrectly keeps the transaction in an abandoned state, even though that's
123-
# no longer the case (after the unclean shutdown, the node's chain returned to the pre-invalidation tip).
124-
# This issue blocks any future spending and results in an incorrect balance display.
122+
# After disconnecting the block, the wallet should record the new best block.
123+
# Upon reload after the crash, since the chainstate was not flushed, the tip contains the previously abandoned
124+
# coinbase. This should be rescanned and now un-abandoned.
125125
wallet = node.get_wallet_rpc("reorg_crash")
126-
assert_equal(wallet.getwalletinfo()['immature_balance'], 0) # FIXME: #31824.
126+
assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False)
127127

128128
# Previously, a bug caused the node to crash if two block disconnection events occurred consecutively.
129129
# Ensure this is no longer the case by simulating a new reorg.

0 commit comments

Comments
 (0)