zcashd/src/validationinterface.cpp

554 lines
27 KiB
C++
Raw Permalink Normal View History

// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2014 The Bitcoin Core developers
// Copyright (c) 2016-2023 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#include "validationinterface.h"
#include "chainparams.h"
#include "init.h"
#include "main.h"
#include "txmempool.h"
#include "ui_interface.h"
#include <boost/thread.hpp>
#include <chrono>
#include <thread>
#include <rust/metrics.h>
using namespace boost::placeholders;
static CMainSignals g_signals;
static constexpr const char* METRIC_WALLET_SYNCED_HEIGHT = "zcashd.wallet.synced.block.height";
CMainSignals& GetMainSignals()
{
return g_signals;
}
void RegisterValidationInterface(CValidationInterface* pwalletIn) {
g_signals.UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1));
g_signals.GetBatchScanner.connect(boost::bind(&CValidationInterface::GetBatchScanner, pwalletIn));
g_signals.SyncTransaction.connect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3));
g_signals.EraseTransaction.connect(boost::bind(&CValidationInterface::EraseFromWallet, pwalletIn, _1));
g_signals.UpdatedTransaction.connect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1));
g_signals.ChainTip.connect(boost::bind(&CValidationInterface::ChainTip, pwalletIn, _1, _2, _3));
g_signals.Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1));
g_signals.BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2));
g_signals.AddressForMining.connect(boost::bind(&CValidationInterface::GetAddressForMining, pwalletIn, _1));
}
void UnregisterValidationInterface(CValidationInterface* pwalletIn) {
g_signals.AddressForMining.disconnect(boost::bind(&CValidationInterface::GetAddressForMining, pwalletIn, _1));
g_signals.BlockChecked.disconnect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2));
g_signals.Broadcast.disconnect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1));
g_signals.ChainTip.disconnect(boost::bind(&CValidationInterface::ChainTip, pwalletIn, _1, _2, _3));
g_signals.UpdatedTransaction.disconnect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1));
g_signals.EraseTransaction.disconnect(boost::bind(&CValidationInterface::EraseFromWallet, pwalletIn, _1));
g_signals.SyncTransaction.disconnect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3));
g_signals.GetBatchScanner.disconnect(boost::bind(&CValidationInterface::GetBatchScanner, pwalletIn));
g_signals.UpdatedBlockTip.disconnect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1));
}
void UnregisterAllValidationInterfaces() {
g_signals.AddressForMining.disconnect_all_slots();
g_signals.BlockChecked.disconnect_all_slots();
g_signals.Broadcast.disconnect_all_slots();
g_signals.ChainTip.disconnect_all_slots();
g_signals.UpdatedTransaction.disconnect_all_slots();
g_signals.EraseTransaction.disconnect_all_slots();
g_signals.SyncTransaction.disconnect_all_slots();
g_signals.GetBatchScanner.disconnect_all_slots();
g_signals.UpdatedBlockTip.disconnect_all_slots();
}
void AddTxToBatches(
std::vector<BatchScanner*> &batchScanners,
const CTransaction &tx,
wallet: Domain-separate batched txids with a "block tag" Previously when a transaction was queried for batch trial decryption, we identified it by its txid. This is sufficient to uniquely identify the transaction within the wallet, but was _not_ sufficient to uniquely identify it within a `ThreadNotifyWallets` loop. In particular, when a reorg occurs, and the same transaction is present in blocks on both sides of the reorg (or is reorged into the mempool and then conflicted out): - The first occurrence would batch the transaction's outputs and store a result receiver. - The second occurrence would overwrite the first occurrence's result receiver with its own. - The first occurrence would read the second's result receiver (which has identical results to the first batch), removing it from the `pending_results` map. - The second occurrence would not find any receiver in the map, and would mark the transaction as having no decrypted results. We fix this by annotating each batched transaction with the hash of the block that triggered it being trial-decrypted: either the block being disconnected, the block being connected, or the null hash to indicate a new transaction in the mempool. This is sufficient to domain-separate all possible sources of duplicate txids: - If a transaction is moved to the mempool via a block disconnection, or from the mempool (either mined or conflicted) via a block connection, its txid will appear twice: once with the block in question's hash, and once with the null hash. - If a transaction is present in both a disconnected and a connected block (mined on both sides of the fork), its txid will appear twice: once each with the two block's txids. Both of the above rely on the assumption that block hashes are collision resistant, which in turn relies on SHA-256 being collision resistant.
2022-07-22 07:38:34 -07:00
const uint256 &blockTag,
const int nHeight)
{
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << tx;
std::vector<unsigned char> txBytes(ssTx.begin(), ssTx.end());
for (auto& batchScanner : batchScanners) {
wallet: Domain-separate batched txids with a "block tag" Previously when a transaction was queried for batch trial decryption, we identified it by its txid. This is sufficient to uniquely identify the transaction within the wallet, but was _not_ sufficient to uniquely identify it within a `ThreadNotifyWallets` loop. In particular, when a reorg occurs, and the same transaction is present in blocks on both sides of the reorg (or is reorged into the mempool and then conflicted out): - The first occurrence would batch the transaction's outputs and store a result receiver. - The second occurrence would overwrite the first occurrence's result receiver with its own. - The first occurrence would read the second's result receiver (which has identical results to the first batch), removing it from the `pending_results` map. - The second occurrence would not find any receiver in the map, and would mark the transaction as having no decrypted results. We fix this by annotating each batched transaction with the hash of the block that triggered it being trial-decrypted: either the block being disconnected, the block being connected, or the null hash to indicate a new transaction in the mempool. This is sufficient to domain-separate all possible sources of duplicate txids: - If a transaction is moved to the mempool via a block disconnection, or from the mempool (either mined or conflicted) via a block connection, its txid will appear twice: once with the block in question's hash, and once with the null hash. - If a transaction is present in both a disconnected and a connected block (mined on both sides of the fork), its txid will appear twice: once each with the two block's txids. Both of the above rely on the assumption that block hashes are collision resistant, which in turn relies on SHA-256 being collision resistant.
2022-07-22 07:38:34 -07:00
batchScanner->AddTransaction(tx, txBytes, blockTag, nHeight);
}
}
void FlushBatches(std::vector<BatchScanner*> &batchScanners) {
for (auto& batchScanner : batchScanners) {
batchScanner->Flush();
}
}
void SyncWithWallets(
std::vector<BatchScanner*> &batchScanners,
const CTransaction &tx,
const CBlock *pblock,
const int nHeight)
{
for (auto& batchScanner : batchScanners) {
batchScanner->SyncTransaction(tx, pblock, nHeight);
}
g_signals.SyncTransaction(tx, pblock, nHeight);
}
struct CachedBlockData {
CBlockIndex *pindex;
MerkleFrontiers oldTrees;
std::list<CTransaction> txConflicted;
CachedBlockData(
CBlockIndex *pindex,
MerkleFrontiers oldTrees,
std::list<CTransaction> txConflicted):
pindex(pindex), oldTrees(oldTrees), txConflicted(txConflicted) {}
};
void ThreadNotifyWallets(CBlockIndex *pindexLastTip)
{
// If pindexLastTip == nullptr, the wallet is at genesis.
// However, the genesis block is not loaded synchronously.
// We need to wait for ThreadImport to finish.
while (pindexLastTip == nullptr) {
{
LOCK(cs_main);
pindexLastTip = chainActive.Genesis();
}
MilliSleep(50);
}
while (true) {
// Run the notifier on an integer second in the steady clock.
auto now = std::chrono::steady_clock::now().time_since_epoch();
auto nextFire = std::chrono::duration_cast<std::chrono::seconds>(
now + std::chrono::seconds(1));
std::this_thread::sleep_until(
std::chrono::time_point<std::chrono::steady_clock>(nextFire));
boost::this_thread::interruption_point();
auto chainParams = Params();
//
// Collect all the state we require
//
// The common ancestor between the last chain tip we notified and the
// current chain tip.
const CBlockIndex *pindexFork;
// The stack of blocks we will notify as having been connected.
// Pushed in reverse, popped in order.
std::vector<CachedBlockData> blockStack;
// Sequence number indicating that we have notified wallets of transactions up to
// the ConnectBlock() call that generated this sequence number.
std::optional<uint64_t> chainNotifiedSequence;
// Transactions that have been recently added to the mempool.
std::pair<std::vector<CTransaction>, uint64_t> recentlyAdded;
{
LOCK(cs_main);
// Figure out the path from the last block we notified to the
// current chain tip.
CBlockIndex *pindex = chainActive.Tip();
pindexFork = chainActive.FindFork(pindexLastTip);
// Iterate backwards over the connected blocks until we have at
// most WALLET_NOTIFY_MAX_BLOCKS to process.
while (pindex && pindex->nHeight > pindexFork->nHeight + WALLET_NOTIFY_MAX_BLOCKS) {
pindex = pindex->pprev;
}
// Iterate backwards over the connected blocks we need to notify.
bool originalTipAtFork = pindex && pindex == pindexFork;
while (pindex && pindex != pindexFork) {
MerkleFrontiers oldFrontiers;
// Get the Sprout commitment tree as of the start of this block.
assert(pcoinsTip->GetSproutAnchorAt(pindex->hashSproutAnchor, oldFrontiers.sprout));
// Get the Sapling commitment tree as of the start of this block.
// We can get this from the `hashFinalSaplingRoot` of the last block
// However, this is only reliable if the last block was on or after
// the Sapling activation height. Otherwise, the last anchor was the
// empty root.
if (chainParams.GetConsensus().NetworkUpgradeActive(
pindex->pprev->nHeight, Consensus::UPGRADE_SAPLING)) {
assert(pcoinsTip->GetSaplingAnchorAt(
pindex->pprev->hashFinalSaplingRoot, oldFrontiers.sapling));
} else {
assert(pcoinsTip->GetSaplingAnchorAt(SaplingMerkleTree::empty_root(), oldFrontiers.sapling));
}
// Get the Orchard Merkle frontier as of the start of this block.
// We can get this from the `hashFinalOrchardRoot` of the last block
// However, this is only reliable if the last block was on or after
// the Orchard activation height. Otherwise, the last anchor was the
// empty root.
if (chainParams.GetConsensus().NetworkUpgradeActive(
pindex->pprev->nHeight, Consensus::UPGRADE_NU5)) {
assert(pcoinsTip->GetOrchardAnchorAt(
pindex->pprev->hashFinalOrchardRoot, oldFrontiers.orchard));
} else {
assert(pcoinsTip->GetOrchardAnchorAt(
OrchardMerkleFrontier::empty_root(), oldFrontiers.orchard));
}
// Fetch recently-conflicted transactions. These will include any
// block that has been connected since the last cycle, but we only
// notify for the conflicts created by the current active chain.
auto recentlyConflicted = TakeRecentlyConflicted(pindex);
blockStack.emplace_back(
pindex,
oldFrontiers,
recentlyConflicted.first);
chainNotifiedSequence = recentlyConflicted.second;
pindex = pindex->pprev;
}
// This conditional can be true in the case that in the interval
// since the last second-boundary, two reorgs occurred: one that
// shifted over to a different chain history, and then a second
// that returned the chain to the original pre-reorg tip. This
// should never occur unless a caller has manually used
// `invalidateblock` to force the second reorg or we have a long
// persistent set of dueling chains. In such a case, wallets may
// not be fully notified of conflicted transactions, but they will
// still have a correct view of the current main chain, and they
// will still be notified properly of the current state of
// transactions in the mempool.
if (originalTipAtFork) {
chainNotifiedSequence = GetChainConnectedSequence();
}
if (chainNotifiedSequence.has_value()) {
recentlyAdded = mempool.DrainRecentlyAdded();
}
}
//
// Execute wallet logic based on the collected state. We MUST NOT take
// the cs_main or mempool.cs locks again until after the next sleep;
// doing so introduces a locking side-channel between this code and the
// network message processing thread.
//
// The wallet inherited from Bitcoin Core was built around the following
// general workflow for moving from one chain tip to another:
//
// - For each block in the old chain, from its tip to the fork point:
// - For each transaction in the block:
// - 1⃣ Trial-decrypt the transaction's shielded outputs.
// - If the transaction belongs to the wallet:
// - 2⃣ Add or update the transaction, and mark it as dirty.
// - Update the wallet's view of the chain tip.
// - 3⃣ In `zcashd`, this is when we decrement note witnesses.
// - For each block in the new chain, from the fork point to its tip:
// - For each transaction that became conflicted by this block:
// - 4⃣ Trial-decrypt the transaction's shielded outputs.
// - If the transaction belongs to the wallet:
// - 5⃣ Add or update the transaction, and mark it as dirty.
// - For each transaction in the block:
// - 6⃣ Trial-decrypt the transaction's shielded outputs.
// - If the transaction belongs to the wallet:
// - 7⃣ Add or update the transaction, and mark it as dirty.
// - Update the wallet's view of the chain tip.
// - 8⃣ In `zcashd`, this is when we increment note witnesses.
// - For each transaction in the mempool:
// - 9⃣ Trial-decrypt the transaction's shielded outputs.
// - If the transaction belongs to the wallet:
// - 🅰️ Add or update the transaction, and mark it as dirty.
//
// Steps 2⃣, 3⃣, 5⃣, 7⃣, 8⃣, and 🅰️ are where wallet state is updated,
// and the relative order of these updates must be preserved in order to
// avoid breaking any internal assumptions that the wallet makes.
//
// Steps 1⃣, 4⃣, 6⃣, and 9⃣ can be performed at any time, as long as
// their results are available when their respective conditionals are
// evaluated. We therefore refactor the above workflow to enable the
// trial-decryption work to be batched and parallelised:
//
// - For each block in the old chain, from its tip to the fork point:
// - For each transaction in the block:
// - Accumulate its Sprout, Sapling, and Orchard outputs.
// - For each block in the new chain, from the fork point to its tip:
// - For each transaction that became conflicted by this block:
// - Accumulate its Sprout, Sapling, and Orchard outputs.
// - For each transaction in the block:
// - Accumulate its Sprout, Sapling, and Orchard outputs.
//
// - 1⃣4⃣6⃣9⃣ Trial-decrypt the Sprout, Sapling, and Orchard outputs.
// - This can split up and batch the work however is most efficient.
//
// - For each block in the old chain, from its tip to the fork point:
// - For each transaction in the block:
// - If the transaction has decrypted outputs, or transparent inputs
// that belong to the wallet:
// - 2⃣ Add or update the transaction, and mark it as dirty.
// - Update the wallet's view of the chain tip.
// - 3⃣ In `zcashd`, this is when we decrement note witnesses.
// - For each block in the new chain, from the fork point to its tip:
// - For each transaction that became conflicted by this block:
// - If the transaction has decrypted outputs, or transparent inputs
// that belong to the wallet:
// - 5⃣ Add or update the transaction, and mark it as dirty.
// - For each transaction in the block:
// - If the transaction has decrypted outputs, or transparent inputs
// that belong to the wallet:
// - 7⃣ Add or update the transaction, and mark it as dirty.
// - Update the wallet's view of the chain tip.
// - 8⃣ In `zcashd`, this is when we increment note witnesses.
// - For each transaction in the mempool:
// - If the transaction has decrypted outputs, or transparent inputs
// that belong to the wallet:
// - 🅰️ Add or update the transaction, and mark it as dirty.
// Get a new handle to the BatchScanner for each listener in each loop.
// This allows the listeners to alter their scanning logic over time,
// for example to add new incoming viewing keys.
auto batchScanners = GetMainSignals().GetBatchScanner();
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Closure that will add a block from blockStack to batchScanners.
auto batchScanConnectedBlock = [&](const CachedBlockData& blockData) {
// Read block from disk.
CBlock block;
if (!ReadBlockFromDisk(block, blockData.pindex, chainParams.GetConsensus())) {
LogPrintf(
"*** %s: Failed to read block %s while collecting shielded outputs from block connects",
__func__, blockData.pindex->GetBlockHash().GetHex());
uiInterface.ThreadSafeMessageBox(
strprintf(_("Error: A fatal internal error occurred, see %s for details"), GetDebugLogPath()),
"", CClientUIInterface::MSG_ERROR);
StartShutdown();
return;
}
// Batch transactions that went from mempool to conflicted:
for (const CTransaction &tx : blockData.txConflicted) {
AddTxToBatches(
batchScanners,
tx,
blockData.pindex->GetBlockHash(),
blockData.pindex->nHeight + 1);
}
// ... and transactions that got confirmed:
for (const CTransaction &tx : block.vtx) {
AddTxToBatches(
batchScanners,
tx,
blockData.pindex->GetBlockHash(),
blockData.pindex->nHeight);
}
};
// Store the iterator pointing to the next block in blockStack that
// should be added to the batch scanners. This will always be valid,
// because:
// - blockStack is only mutated via std::vector::pop_back() below this
// point, which only invalidates std::vector::end() and iterators to
// the last element of the vector.
// - We check below that blockStack.rbegin() != blockStackScanned before
// any call to blockStack.pop_back(), which ensures that we never pop
// the block that blockStackScanned is pointing to.
auto blockStackScanned = blockStack.rbegin();
if (!batchScanners.empty()) {
// Batch the shielded outputs across all blocks being processed.
// TODO: We can probably not bother trial-decrypting transactions
// in blocks being disconnected, or that are becoming conflicted,
// instead doing a plain "is this tx in the wallet" check. However,
// the logic in AddToWalletIfInvolvingMe would need to be carefully
// checked to ensure its side-effects are correctly preserved, so
// for now we maintain the previous behaviour of trial-decrypting
// everything.
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Batch block disconnects. We always add every disconnected block
// to batchScanners, because this will be at most 100 blocks, which
// is at most 2GB, but in practice will be much less than this, and
// the growth problem in the interaction between this thread and the
// main node is in connected blocks. And per the above TODO, this
// might eventually be removed entirely.
auto pindexScan = pindexLastTip;
while (pindexScan && pindexScan != pindexFork) {
// Read block from disk.
CBlock block;
if (!ReadBlockFromDisk(block, pindexScan, chainParams.GetConsensus())) {
LogPrintf(
"*** %s: Failed to read block %s while collecting shielded outputs",
__func__, pindexScan->GetBlockHash().GetHex());
uiInterface.ThreadSafeMessageBox(
strprintf(_("Error: A fatal internal error occurred, see %s for details"), GetDebugLogPath()),
"", CClientUIInterface::MSG_ERROR);
StartShutdown();
return;
}
// Batch transactions that went from 1-confirmed to 0-confirmed
// or conflicted.
for (const CTransaction &tx : block.vtx) {
wallet: Domain-separate batched txids with a "block tag" Previously when a transaction was queried for batch trial decryption, we identified it by its txid. This is sufficient to uniquely identify the transaction within the wallet, but was _not_ sufficient to uniquely identify it within a `ThreadNotifyWallets` loop. In particular, when a reorg occurs, and the same transaction is present in blocks on both sides of the reorg (or is reorged into the mempool and then conflicted out): - The first occurrence would batch the transaction's outputs and store a result receiver. - The second occurrence would overwrite the first occurrence's result receiver with its own. - The first occurrence would read the second's result receiver (which has identical results to the first batch), removing it from the `pending_results` map. - The second occurrence would not find any receiver in the map, and would mark the transaction as having no decrypted results. We fix this by annotating each batched transaction with the hash of the block that triggered it being trial-decrypted: either the block being disconnected, the block being connected, or the null hash to indicate a new transaction in the mempool. This is sufficient to domain-separate all possible sources of duplicate txids: - If a transaction is moved to the mempool via a block disconnection, or from the mempool (either mined or conflicted) via a block connection, its txid will appear twice: once with the block in question's hash, and once with the null hash. - If a transaction is present in both a disconnected and a connected block (mined on both sides of the fork), its txid will appear twice: once each with the two block's txids. Both of the above rely on the assumption that block hashes are collision resistant, which in turn relies on SHA-256 being collision resistant.
2022-07-22 07:38:34 -07:00
AddTxToBatches(batchScanners, tx, block.GetHash(), pindexScan->nHeight);
}
// On to the next block!
pindexScan = pindexScan->pprev;
}
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Add block connections to batchScanners until we have either added
// the entirety of blockStack, or have reached the memory limit.
//
// We process blockStack in the same order we do below, so batched
// work can be completed in roughly the order we need it.
for (; blockStackScanned != blockStack.rend(); ++blockStackScanned) {
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
const auto& blockData = *blockStackScanned;
batchScanConnectedBlock(blockData);
}
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Batch transactions in the mempool. It's fine that we always add
// these to batchScanners; if we reached the memory limit earlier,
// it just means that the mempool results will be completed earlier
// than they are required, and will sit in memory for a bit longer.
// The ZIP 401 mempool limits are around 80MB, which is well below
// the limits we are concerned with here.
for (auto& tx : recentlyAdded.first) {
wallet: Domain-separate batched txids with a "block tag" Previously when a transaction was queried for batch trial decryption, we identified it by its txid. This is sufficient to uniquely identify the transaction within the wallet, but was _not_ sufficient to uniquely identify it within a `ThreadNotifyWallets` loop. In particular, when a reorg occurs, and the same transaction is present in blocks on both sides of the reorg (or is reorged into the mempool and then conflicted out): - The first occurrence would batch the transaction's outputs and store a result receiver. - The second occurrence would overwrite the first occurrence's result receiver with its own. - The first occurrence would read the second's result receiver (which has identical results to the first batch), removing it from the `pending_results` map. - The second occurrence would not find any receiver in the map, and would mark the transaction as having no decrypted results. We fix this by annotating each batched transaction with the hash of the block that triggered it being trial-decrypted: either the block being disconnected, the block being connected, or the null hash to indicate a new transaction in the mempool. This is sufficient to domain-separate all possible sources of duplicate txids: - If a transaction is moved to the mempool via a block disconnection, or from the mempool (either mined or conflicted) via a block connection, its txid will appear twice: once with the block in question's hash, and once with the null hash. - If a transaction is present in both a disconnected and a connected block (mined on both sides of the fork), its txid will appear twice: once each with the two block's txids. Both of the above rely on the assumption that block hashes are collision resistant, which in turn relies on SHA-256 being collision resistant.
2022-07-22 07:38:34 -07:00
AddTxToBatches(batchScanners, tx, uint256(), pindexLastTip->nHeight + 1);
}
}
// Ensure that all pending work has been started.
FlushBatches(batchScanners);
// Notify block disconnects
while (pindexLastTip && pindexLastTip != pindexFork) {
// Read block from disk.
CBlock block;
if (!ReadBlockFromDisk(block, pindexLastTip, chainParams.GetConsensus())) {
LogPrintf(
"*** %s: Failed to read block %s while notifying wallets of block disconnects",
__func__, pindexLastTip->GetBlockHash().GetHex());
uiInterface.ThreadSafeMessageBox(
strprintf(_("Error: A fatal internal error occurred, see %s for details"), GetDebugLogPath()),
"", CClientUIInterface::MSG_ERROR);
StartShutdown();
return;
}
// Let wallets know transactions went from 1-confirmed to
// 0-confirmed or conflicted:
for (const CTransaction &tx : block.vtx) {
SyncWithWallets(batchScanners, tx, NULL, pindexLastTip->nHeight);
}
// Update cached incremental witnesses
// This will take the cs_main lock in order to obtain the CBlockLocator
// used by `SetBestChain`, but as that write only occurs once every
// WRITE_WITNESS_INTERVAL * 1000000 microseconds this should not be
// exploitable as a timing channel.
GetMainSignals().ChainTip(pindexLastTip, &block, std::nullopt);
// On to the next block!
pindexLastTip = pindexLastTip->pprev;
MetricsGauge(METRIC_WALLET_SYNCED_HEIGHT, pindexLastTip->nHeight);
}
// Notify block connections
while (!blockStack.empty()) {
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Add more block connections to batchScanners until we have either
// added the rest of blockStack, or have reached the memory limit
// again. At this point, we know that blockStackScanned has not been
// invalidated by mutations to blockStack, and can be dereferenced.
for (; blockStackScanned != blockStack.rend(); ++blockStackScanned) {
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
const auto& blockData = *blockStackScanned;
batchScanConnectedBlock(blockData);
}
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Ensure that all pending work has been started.
FlushBatches(batchScanners);
// Sync with the wallet until either:
// - there are no more blocks to sync, and we are done;
// - we have synced every block that batchScanners has been given,
// and can make no more forward progress; or
// - we have more blocks that we _could_ add to batchScanners, and
// are back below the memory limit (so should create more batches
// before continuing, for optimal pipelining).
while (!(
blockStack.empty() ||
blockStack.rbegin() == blockStackScanned ||
(blockStackScanned != blockStack.rend())
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
)) {
auto& blockData = blockStack.back();
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Read block from disk.
CBlock block;
if (!ReadBlockFromDisk(block, blockData.pindex, chainParams.GetConsensus())) {
LogPrintf(
"*** %s: Failed to read block %s while notifying wallets of block connects",
__func__, blockData.pindex->GetBlockHash().GetHex());
uiInterface.ThreadSafeMessageBox(
strprintf(_("Error: A fatal internal error occurred, see %s for details"), GetDebugLogPath()),
"", CClientUIInterface::MSG_ERROR);
StartShutdown();
return;
}
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
// Tell wallet about transactions that went from mempool
// to conflicted:
for (const CTransaction &tx : blockData.txConflicted) {
SyncWithWallets(batchScanners, tx, NULL, blockData.pindex->nHeight + 1);
}
// ... and about transactions that got confirmed:
for (const CTransaction &tx : block.vtx) {
SyncWithWallets(batchScanners, tx, &block, blockData.pindex->nHeight);
}
// Update cached incremental witnesses
// This will take the cs_main lock in order to obtain the CBlockLocator
// used by `SetBestChain`, but as that write only occurs once every
// WRITE_WITNESS_INTERVAL * 1000000 microseconds this should not be
// exploitable as a timing channel.
GetMainSignals().ChainTip(blockData.pindex, &block, blockData.oldTrees);
// Notify UI to display prev block's coinbase if it was ours.
static uint256 hashPrevBestCoinBase;
GetMainSignals().UpdatedTransaction(hashPrevBestCoinBase);
hashPrevBestCoinBase = block.vtx[0].GetHash();
// This block is done!
pindexLastTip = blockData.pindex;
MetricsGauge(METRIC_WALLET_SYNCED_HEIGHT, pindexLastTip->nHeight);
wallet: Refactor `ThreadNotifyWallets` to support batch memory limits ThreadNotifyWallets was created in order to decouple wallet scanning of transactions from the main chain updates, avoiding timing leaks to network peers. To further ensure that not even lock contention could be used to extract timing information, ThreadNotifyWallets collects update information from the main chain on integer second boundaries. In general this means that the wallet is processing the last second's worth of blocks between each synchronization point. However, when there are sequences of blocks that are costly for the wallet to scan, it may take the wallet longer than a second to process a second's worth of blocks connected to the main chain. This means that ThreadNotifyWallets needs to wait until the next integer second boundary before collecting the next set of updates, which means it will be processing at least two seconds' worth of blocks. For extended periods where the chain contains many outputs, the wallet will get progressively further behind the main chain. At the time that ThreadNotifyWallets was created, the above behaviour was fine, because while the wallet scanning process consumed a lot of CPU time, it did not consume much memory (as blocks are stored on disk). However, we recently added batch scanning, which requires allocating memory for each output that is being scanned. For a wallet with a growing gap between its scanned-to height and the chain tip height, the size of the necessary allocation will grow with each integer second boundary crossed, until the node reaches OOM. The solution is to implement backpressure: if we reach a memory limit before we've finished adding blocks to the batch scanners, then we start consuming the results from the batch scanner to free up memory space for subsequent batches. This commit implements the logic necessary to interleave batch creation and batch result consumption. It does not apply any batch memory limits, and as such should be effectively a no-op refactor. Co-authored-by: Daira Hopwood <daira@jacaranda.org>
2022-09-19 15:28:55 -07:00
assert(blockStack.rbegin() != blockStackScanned);
blockStack.pop_back();
}
}
// Notify transactions in the mempool
for (auto& tx : recentlyAdded.first) {
try {
SyncWithWallets(batchScanners, tx, NULL, pindexLastTip->nHeight + 1);
} catch (const boost::thread_interrupted&) {
throw;
} catch (const std::exception& e) {
PrintExceptionContinue(&e, "ThreadNotifyWallets()");
} catch (...) {
PrintExceptionContinue(NULL, "ThreadNotifyWallets()");
}
}
// Update the notified sequence numbers. We only need this in regtest mode,
// and should not lock on cs or cs_main here otherwise.
if (chainParams.NetworkIDString() == "regtest") {
if (chainNotifiedSequence.has_value()) {
SetChainNotifiedSequence(chainParams, chainNotifiedSequence.value());
}
if (recentlyAdded.second > 0) {
mempool.SetNotifiedSequence(recentlyAdded.second);
}
}
}
}