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.
This commit is contained in:
Ava Chow
2024-04-26 17:12:52 -04:00
parent 9a05b45da6
commit 7bacabb204
4 changed files with 62 additions and 32 deletions

View File

@@ -106,7 +106,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
LOCK(wallet.cs_wallet);
LOCK(Assert(m_node.chainman)->GetMutex());
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash());
}
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
@@ -116,8 +116,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
{
CBlockLocator locator;
BOOST_CHECK(!WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
BOOST_CHECK(locator.IsNull());
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
}
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)
{
CBlockLocator locator;
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
BOOST_CHECK(!locator.IsNull());
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
}
}

View File

@@ -663,6 +663,22 @@ void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc)
batch.WriteBestBlock(loc);
}
void CWallet::SetLastBlockProcessedInMem(int block_height, uint256 block_hash)
{
AssertLockHeld(cs_wallet);
m_last_block_processed = block_hash;
m_last_block_processed_height = block_height;
}
void CWallet::SetLastBlockProcessed(int block_height, uint256 block_hash)
{
AssertLockHeld(cs_wallet);
SetLastBlockProcessedInMem(block_height, block_hash);
WriteBestBlock();
}
void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in)
{
LOCK(cs_wallet);
@@ -1378,15 +1394,16 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
}
}
void CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
bool CWallet::SyncTransaction(const CTransactionRef& ptx, const SyncTxState& state, bool update_tx, bool rescanning_old_block)
{
if (!AddToWalletIfInvolvingMe(ptx, state, update_tx, rescanning_old_block))
return; // Not one of ours
return false; // Not one of ours
// If a transaction changes 'conflicted' state, that changes the balance
// available of the outputs it spends. So force those to be
// recomputed, also:
MarkInputsDirty(ptx);
return true;
}
void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
@@ -1473,18 +1490,25 @@ void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& b
assert(block.data);
LOCK(cs_wallet);
m_last_block_processed_height = block.height;
m_last_block_processed = block.hash;
// Update the best block in memory first. This will set the best block's height, which is
// needed by MarkConflicted.
SetLastBlockProcessedInMem(block.height, block.hash);
// No need to scan block if it was created before the wallet birthday.
// Uses chain max time and twice the grace period to adjust time for block time variability.
if (block.chain_time_max < m_birth_time.load() - (TIMESTAMP_WINDOW * 2)) return;
// Scan block
bool wallet_updated = false;
for (size_t index = 0; index < block.data->vtx.size(); index++) {
SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
wallet_updated |= SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast<int>(index)});
transactionRemovedFromMempool(block.data->vtx[index], MemPoolRemovalReason::BLOCK);
}
// Update on disk if this block resulted in us updating a tx, or periodically every 144 blocks (~1 day)
if (wallet_updated || block.height % 144 == 0) {
WriteBestBlock();
}
}
void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
@@ -1496,9 +1520,6 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
// be unconfirmed, whether or not the transaction is added back to the mempool.
// User may have to call abandontransaction again. It may be addressed in the
// future with a stickier abandoned state or even removing abandontransaction call.
m_last_block_processed_height = block.height - 1;
m_last_block_processed = *Assert(block.prev_hash);
int disconnect_height = block.height;
for (size_t index = 0; index < block.data->vtx.size(); index++) {
@@ -1532,6 +1553,9 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
}
}
}
// Update the best block
SetLastBlockProcessed(block.height - 1, *Assert(block.prev_hash));
}
void CWallet::updatedBlockTip()
@@ -3264,14 +3288,7 @@ void CWallet::postInitProcess()
bool CWallet::BackupWallet(const std::string& strDest) const
{
if (m_chain) {
CBlockLocator loc;
WITH_LOCK(cs_wallet, chain().findBlock(m_last_block_processed, FoundBlock().locator(loc)));
if (!loc.IsNull()) {
WalletBatch batch(GetDatabase());
batch.WriteBestBlock(loc);
}
}
WITH_LOCK(cs_wallet, WriteBestBlock());
return GetDatabase().Backup(strDest);
}
@@ -4453,4 +4470,17 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
}
return std::nullopt;
}
void CWallet::WriteBestBlock() const
{
AssertLockHeld(cs_wallet);
if (!m_last_block_processed.IsNull()) {
CBlockLocator loc;
chain().findBlock(m_last_block_processed, FoundBlock().locator(loc));
WalletBatch batch(GetDatabase());
batch.WriteBestBlock(loc);
}
}
} // namespace wallet

View File

@@ -370,7 +370,7 @@ private:
void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void SyncTransaction(const CTransactionRef& tx, const SyncTxState& state, bool update_tx = true, bool rescanning_old_block = false) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool SyncTransaction(const CTransactionRef& tx, const SyncTxState& state, bool update_tx = true, bool rescanning_old_block = false) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** WalletFlags set on this wallet. */
std::atomic<uint64_t> m_wallet_flags{0};
@@ -437,6 +437,9 @@ private:
static NodeClock::time_point GetDefaultNextResend();
// Update last block processed in memory only
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
public:
/**
* Main wallet lock.
@@ -974,13 +977,10 @@ public:
assert(m_last_block_processed_height >= 0);
return m_last_block_processed;
}
/** Set last block processed height, currently only use in unit test */
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
{
AssertLockHeld(cs_wallet);
m_last_block_processed_height = block_height;
m_last_block_processed = block_hash;
};
/** Set last block processed height, and write to database */
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** Write the current best block to database */
void WriteBestBlock() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Connect the signals from ScriptPubKeyMans to the signals in CWallet
void ConnectScriptPubKeyManNotifiers();

View File

@@ -119,11 +119,11 @@ class ReorgsRestoreTest(BitcoinTestFramework):
self.start_node(0)
assert_equal(node.getbestblockhash(), tip)
# Due to an existing bug, the wallet incorrectly keeps the transaction in an abandoned state, even though that's
# no longer the case (after the unclean shutdown, the node's chain returned to the pre-invalidation tip).
# This issue blocks any future spending and results in an incorrect balance display.
# After disconnecting the block, the wallet should record the new best block.
# Upon reload after the crash, since the chainstate was not flushed, the tip contains the previously abandoned
# coinbase. This should be rescanned and now un-abandoned.
wallet = node.get_wallet_rpc("reorg_crash")
assert_equal(wallet.getwalletinfo()['immature_balance'], 0) # FIXME: #31824.
assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False)
# Previously, a bug caused the node to crash if two block disconnection events occurred consecutively.
# Ensure this is no longer the case by simulating a new reorg.