diff --git a/ci/dash/lint-tidy.sh b/ci/dash/lint-tidy.sh index 104ae29083e3..b17c0860668f 100755 --- a/ci/dash/lint-tidy.sh +++ b/ci/dash/lint-tidy.sh @@ -105,6 +105,7 @@ iwyu_tool.py \ "src/node/chainstate.cpp" \ "src/node/chainstatemanager_args.cpp" \ "src/node/mempool_args.cpp" \ + "src/node/utxo_snapshot.cpp" \ "src/node/validation_cache_args.cpp" \ "src/node/minisketchwrapper.cpp" \ "src/policy/feerate.cpp" \ diff --git a/ci/test/00_setup_env.sh b/ci/test/00_setup_env.sh index 3ebb2140afee..33d878d7b20a 100755 --- a/ci/test/00_setup_env.sh +++ b/ci/test/00_setup_env.sh @@ -8,7 +8,6 @@ export LC_ALL=C.UTF-8 # The root dir. # The ci system copies this folder. -# This is where the depends build is done. BASE_ROOT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../../ >/dev/null 2>&1 && pwd ) export BASE_ROOT_DIR @@ -62,18 +61,20 @@ export CCACHE_MAXSIZE=${CCACHE_MAXSIZE:-100M} export CCACHE_TEMPDIR=${CCACHE_TEMPDIR:-/tmp/.ccache-temp} export CCACHE_COMPRESS=${CCACHE_COMPRESS:-1} # The cache dir. -# This folder exists on the ci host and ci guest. Changes are propagated back and forth. +# This folder exists only on the ci guest, and on the ci host as a volume. export CCACHE_DIR=${CCACHE_DIR:-$CACHE_DIR/ccache} # ctcache (clang-tidy cache) configuration export CTCACHE_DIR=${CTCACHE_DIR:-$CACHE_DIR/ctcache} export CTCACHE_MAXSIZE_MB=${CTCACHE_MAXSIZE_MB:-50} # The depends dir. -# This folder exists on the ci host and ci guest. Changes are propagated back and forth. +# This folder exists only on the ci guest, and on the ci host as a volume. export DEPENDS_DIR=${DEPENDS_DIR:-$BASE_ROOT_DIR/depends} # Folder where the build result is put (bin and lib). export BASE_OUTDIR=${BASE_OUTDIR:-$BASE_SCRATCH_DIR/out/$HOST} # Folder where the build is done (dist and out-of-tree build). export BASE_BUILD_DIR=${BASE_BUILD_DIR:-$BASE_SCRATCH_DIR/build-ci} +# The folder for previous release binaries. +# This folder exists only on the ci guest, and on the ci host as a volume. export PREVIOUS_RELEASES_DIR=${PREVIOUS_RELEASES_DIR:-$BASE_ROOT_DIR/releases/$HOST} export SDK_URL=${SDK_URL:-https://bitcoincore.org/depends-sources/sdks} export DOCKER_PACKAGES=${DOCKER_PACKAGES:-build-essential libtool autotools-dev automake pkg-config bsdmainutils curl ca-certificates ccache python3 rsync git procps} diff --git a/src/hash.h b/src/hash.h index 3a79b7c0f879..0abe93aa2793 100644 --- a/src/hash.h +++ b/src/hash.h @@ -167,6 +167,39 @@ class CHashWriter : public HashWriter }; /** Reads data from an underlying stream, while hashing the read data. */ +template +class HashVerifier : public HashWriter +{ +private: + Source& m_source; + +public: + explicit HashVerifier(Source& source LIFETIMEBOUND) : m_source{source} {} + + void read(Span dst) + { + m_source.read(dst); + this->write(dst); + } + + void ignore(size_t num_bytes) + { + std::byte data[1024]; + while (num_bytes > 0) { + size_t now = std::min(num_bytes, 1024); + read({data, now}); + num_bytes -= now; + } + } + + template + HashVerifier& operator>>(T&& obj) + { + ::Unserialize(*this, obj); + return *this; + } +}; + template class CHashVerifier : public CHashWriter { diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 1f2680fa0a6c..99c701420e00 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -384,7 +384,7 @@ bool BlockManager::LoadBlockIndexDB() } for (std::set::iterator it = setBlkDataFiles.begin(); it != setBlkDataFiles.end(); it++) { FlatFilePos pos(*it, 0); - if (CAutoFile(OpenBlockFile(pos, true), SER_DISK, CLIENT_VERSION).IsNull()) { + if (AutoFile{OpenBlockFile(pos, true)}.IsNull()) { return false; } } @@ -509,14 +509,14 @@ CBlockFileInfo* BlockManager::GetBlockFileInfo(size_t n) static bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const uint256& hashBlock, const CMessageHeader::MessageStartChars& messageStart) { // Open history file to append - CAutoFile fileout(OpenUndoFile(pos), SER_DISK, CLIENT_VERSION); + AutoFile fileout{OpenUndoFile(pos)}; if (fileout.IsNull()) { LogError("%s: OpenUndoFile failed\n", __func__); return false; } // Write index header - unsigned int nSize = GetSerializeSize(blockundo, fileout.GetVersion()); + unsigned int nSize = GetSerializeSize(blockundo, CLIENT_VERSION); fileout << messageStart << nSize; // Write undo data @@ -547,7 +547,7 @@ bool UndoReadFromDisk(CBlockUndo& blockundo, const CBlockIndex* pindex) } // Open history file to read - CAutoFile filein(OpenUndoFile(pos, true), SER_DISK, CLIENT_VERSION); + AutoFile filein{OpenUndoFile(pos, true)}; if (filein.IsNull()) { LogError("%s: OpenUndoFile failed\n", __func__); return false; @@ -555,7 +555,7 @@ bool UndoReadFromDisk(CBlockUndo& blockundo, const CBlockIndex* pindex) // Read block uint256 hashChecksum; - CHashVerifier verifier(&filein); // We need a CHashVerifier as reserializing may lose data + HashVerifier verifier{filein}; // Use HashVerifier as reserializing may lose data, c.f. commit d342424301013ec47dc146a4beb49d5c9319d80a try { verifier << pindex->pprev->GetBlockHash(); verifier >> blockundo; @@ -748,14 +748,14 @@ bool BlockManager::FindUndoPos(BlockValidationState& state, int nFile, FlatFileP static bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos, const CMessageHeader::MessageStartChars& messageStart) { // Open history file to append - CAutoFile fileout(OpenBlockFile(pos), SER_DISK, CLIENT_VERSION); + AutoFile fileout{OpenBlockFile(pos)}; if (fileout.IsNull()) { LogError("WriteBlockToDisk: OpenBlockFile failed\n"); return false; } // Write index header - unsigned int nSize = GetSerializeSize(block, fileout.GetVersion()); + unsigned int nSize = GetSerializeSize(block, CLIENT_VERSION); fileout << messageStart << nSize; // Write block @@ -807,7 +807,7 @@ std::optional ReadBlockFromDisk(CBlock& block, const FlatFilePos& pos, block.SetNull(); // Open history file to read - CAutoFile filein(OpenBlockFile(pos, true), SER_DISK, CLIENT_VERSION); + AutoFile filein{OpenBlockFile(pos, true)}; if (filein.IsNull()) { LogError("ReadBlockFromDisk: OpenBlockFile failed for %s\n", pos.ToString()); return std::nullopt; diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 70f1547fb1c4..02d259d3a868 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -168,7 +168,6 @@ class BlockManager /** * All pairs A->B, where A (or one of its ancestors) misses transactions, but B has transactions. - * Pruned nodes may have entries where B is missing data. */ std::multimap m_blocks_unlinked; diff --git a/src/node/utxo_snapshot.cpp b/src/node/utxo_snapshot.cpp index 9a9e4332ea83..08a6976aadb9 100644 --- a/src/node/utxo_snapshot.cpp +++ b/src/node/utxo_snapshot.cpp @@ -6,14 +6,19 @@ #include #include +#include +#include +#include #include #include #include #include #include +#include #include #include +#include namespace node { diff --git a/src/node/utxo_snapshot.h b/src/node/utxo_snapshot.h index 0c46960b8d38..62cc4c55c82b 100644 --- a/src/node/utxo_snapshot.h +++ b/src/node/utxo_snapshot.h @@ -7,11 +7,16 @@ #define BITCOIN_NODE_UTXO_SNAPSHOT_H #include -#include +#include #include -#include +#include +#include +#include #include +#include + +class Chainstate; namespace node { //! Metadata describing a serialized version of a UTXO set from which an diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 4d522c47083d..f69b6651b700 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -343,6 +343,16 @@ void OverviewPage::setWalletModel(WalletModel *model) } } +// Only show the most recent m_num_items rows +void OverviewPage::LimitTransactionRows() +{ + if (filter && ui->listTransactions && ui->listTransactions->model() && filter.get() == ui->listTransactions->model()) { + for (int i = 0; i < filter->rowCount(); ++i) { + ui->listTransactions->setRowHidden(i, i >= m_num_items); + } + } +} + void OverviewPage::updateDisplayUnit() { if (walletModel && walletModel->getOptionsModel()) { @@ -761,13 +771,19 @@ void OverviewPage::SetupTransactionList(int nNumItems) filter->setShowInactive(false); filter->sort(TransactionTableModel::Date, Qt::DescendingOrder); ui->listTransactions->setModel(filter.get()); + + connect(filter.get(), &TransactionFilterProxy::rowsInserted, this, &OverviewPage::LimitTransactionRows); + connect(filter.get(), &TransactionFilterProxy::rowsRemoved, this, &OverviewPage::LimitTransactionRows); + connect(filter.get(), &TransactionFilterProxy::rowsMoved, this, &OverviewPage::LimitTransactionRows); + connect(filter.get(), &TransactionFilterProxy::modelReset, this, &OverviewPage::LimitTransactionRows); } - if (filter->rowCount() == nNumItems) { + if (m_num_items == nNumItems) { return; } + m_num_items = nNumItems; + LimitTransactionRows(); - filter->setLimit(nNumItems); ui->listTransactions->setMinimumHeight(nNumItems * ITEM_HEIGHT); } diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 15a9c0f02a2c..5902bc2e2945 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -63,11 +63,14 @@ public Q_SLOTS: TxViewDelegate *txdelegate; std::unique_ptr filter; + //! Number of most recent transactions the overview list keeps visible + int m_num_items{0}; void SetupTransactionList(int nNumItems); void DisableCoinJoinCompletely(); private Q_SLOTS: + void LimitTransactionRows(); void toggleCoinJoin(); void updateDisplayUnit(); void updateCoinJoinProgress(); diff --git a/src/qt/transactionfilterproxy.cpp b/src/qt/transactionfilterproxy.cpp index 12758886ad63..1a59e2b5802a 100644 --- a/src/qt/transactionfilterproxy.cpp +++ b/src/qt/transactionfilterproxy.cpp @@ -88,27 +88,8 @@ void TransactionFilterProxy::setWatchOnlyFilter(WatchOnlyFilter filter) invalidateFilter(); } -void TransactionFilterProxy::setLimit(int limit) -{ - Q_EMIT layoutAboutToBeChanged(); - this->limitRows = limit; - Q_EMIT layoutChanged(); -} - void TransactionFilterProxy::setShowInactive(bool _showInactive) { this->showInactive = _showInactive; invalidateFilter(); } - -int TransactionFilterProxy::rowCount(const QModelIndex &parent) const -{ - if(limitRows != -1) - { - return std::min(QSortFilterProxyModel::rowCount(parent), limitRows); - } - else - { - return QSortFilterProxyModel::rowCount(parent); - } -} diff --git a/src/qt/transactionfilterproxy.h b/src/qt/transactionfilterproxy.h index 5d81ec13a75f..cb102e2a2af6 100644 --- a/src/qt/transactionfilterproxy.h +++ b/src/qt/transactionfilterproxy.h @@ -59,14 +59,9 @@ class TransactionFilterProxy : public QSortFilterProxyModel void setMinAmount(const CAmount& minimum); void setWatchOnlyFilter(WatchOnlyFilter filter); - /** Set maximum number of rows returned, -1 if unlimited. */ - void setLimit(int limit); - /** Set whether to show conflicted transactions. */ void setShowInactive(bool showInactive); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - protected: bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; @@ -77,7 +72,6 @@ class TransactionFilterProxy : public QSortFilterProxyModel quint32 typeFilter; WatchOnlyFilter watchOnlyFilter{WatchOnlyFilter_All}; CAmount minAmount{0}; - int limitRows{-1}; bool showInactive{true}; }; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6c2a5efc689f..074e29cec3ef 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ #include #include #include +#include #include #include @@ -2833,17 +2835,52 @@ class BlockFiltersScanReserver } }; +static bool CheckBlockFilterMatches(BlockManager& blockman, const CBlockIndex& blockindex, const GCSFilter::ElementSet& needles) +{ + const CBlock block{GetBlockChecked(blockman, &blockindex)}; + const CBlockUndo block_undo{GetUndoChecked(blockman, &blockindex)}; + + // Check if any of the outputs match the scriptPubKey + for (const auto& tx : block.vtx) { + if (std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& txout) { + return needles.count(std::vector(txout.scriptPubKey.begin(), txout.scriptPubKey.end())) != 0; + })) { + return true; + } + bool special_tx_match{false}; + ExtractSpecialTxFilterElements(*tx, [&](Span element) { + special_tx_match = special_tx_match || needles.count(std::vector(element.begin(), element.end())) != 0; + }); + if (special_tx_match) return true; + } + // Check if any of the inputs match the scriptPubKey + for (const auto& txundo : block_undo.vtxundo) { + if (std::any_of(txundo.vprevout.cbegin(), txundo.vprevout.cend(), [&](const auto& coin) { + return needles.count(std::vector(coin.out.scriptPubKey.begin(), coin.out.scriptPubKey.end())) != 0; + })) { + return true; + } + } + + return false; +} + static RPCHelpMan scanblocks() { return RPCHelpMan{"scanblocks", - "\nReturn relevant blockhashes for given descriptors.\n" + "\nReturn relevant blockhashes for given descriptors (requires blockfilterindex).\n" "This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)", { scan_action_arg_desc, scan_objects_arg_desc, RPCArg{"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "Height to start to scan from"}, RPCArg{"stop_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"chain tip"}, "Height to stop to scan"}, - RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC_FILTER)}, "The type name of the filter"} + RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC_FILTER)}, "The type name of the filter"}, + RPCArg{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"filter_false_positives", RPCArg::Type::BOOL, RPCArg::Default{false}, "Filter false positives (slower and may fail on pruned nodes). Otherwise they may occur at a rate of 1/M"}, + }, + RPCArgOptions{.oneline_description="\"options\""}}, }, { scan_result_status_none, @@ -2903,6 +2940,9 @@ static RPCHelpMan scanblocks() throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype"); } + UniValue options{request.params[5].isNull() ? UniValue::VOBJ : request.params[5]}; + bool filter_false_positives{options.exists("filter_false_positives") ? options["filter_false_positives"].get_bool() : false}; + BlockFilterIndex* index = GetBlockFilterIndex(filtertype); if (!index) { throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name); @@ -2973,6 +3013,15 @@ static RPCHelpMan scanblocks() for (const BlockFilter& filter : filters) { // compare the elements-set with each filter if (filter.GetFilter().MatchAny(needle_set)) { + if (filter_false_positives) { + // Double check the filter matches by scanning the block + const CBlockIndex& blockindex = *CHECK_NONFATAL(WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(filter.GetBlockHash()))); + + if (!CheckBlockFilterMatches(chainman.m_blockman, blockindex, needle_set)) { + continue; + } + } + blocks.push_back(filter.GetBlockHash().GetHex()); LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex()); } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1cba059e4732..e35183e739f1 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -110,6 +110,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "scanblocks", 1, "scanobjects" }, { "scanblocks", 2, "start_height" }, { "scanblocks", 3, "stop_height" }, + { "scanblocks", 5, "options" }, { "scantxoutset", 1, "scanobjects" }, { "addmultisigaddress", 0, "nrequired" }, { "addmultisigaddress", 1, "keys" }, diff --git a/src/sync.h b/src/sync.h index 5b0d73ddb555..bb5863982a3e 100644 --- a/src/sync.h +++ b/src/sync.h @@ -11,7 +11,7 @@ #include #endif -#include +#include // IWYU pragma: export #include #include diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h index f8199fbb435b..757eeb5a5c28 100644 --- a/src/wallet/transaction.h +++ b/src/wallet/transaction.h @@ -311,6 +311,7 @@ class CWalletTx bool isAbandoned() const { return state() && state()->abandoned; } bool isConflicted() const { return state(); } + bool isInactive() const { return state(); } bool isUnconfirmed() const { return !isAbandoned() && !isConflicted() && !isConfirmed(); } bool isConfirmed() const { return state(); } const uint256& GetHash() const LIFETIMEBOUND { return tx->GetHash(); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 14a79964e222..3ef0183ac807 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1091,6 +1091,34 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const CheckAndLockDustOutputs(hash, batch); } + // Mark inactive coinbase transactions and their descendants as abandoned + if (wtx.IsCoinBase() && wtx.isInactive()) { + std::vector txs{&wtx}; + + TxStateInactive inactive_state = TxStateInactive{/*abandoned=*/true}; + + while (!txs.empty()) { + CWalletTx* desc_tx = txs.back(); + txs.pop_back(); + desc_tx->m_state = inactive_state; + // Break caches since we have changed the state + desc_tx->MarkDirty(); + batch.WriteTx(*desc_tx); + MarkInputsDirty(desc_tx->tx); + ReconcileWalletUTXOs(desc_tx->tx, batch); + for (unsigned int i = 0; i < desc_tx->tx->vout.size(); ++i) { + COutPoint outpoint(desc_tx->GetHash(), i); + std::pair range = mapTxSpends.equal_range(outpoint); + for (TxSpends::const_iterator it = range.first; it != range.second; ++it) { + const auto wit = mapWallet.find(it->second); + if (wit != mapWallet.end()) { + txs.push_back(&wit->second); + } + } + } + } + } + //// debug print WalletLogPrintf("AddToWallet %s %s%s %s\n", hash.ToString(), (fInsertedNew ? "new" : ""), (fUpdated ? "update" : ""), TxStateString(state)); @@ -1348,7 +1376,11 @@ bool CWallet::AbandonTransaction(const uint256& hashTx) return TxUpdate::UNCHANGED; }; - // Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too + // Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too. + // States are not permanent, so these transactions can become unabandoned if they are re-added to the + // mempool, or confirmed in a block, or conflicted. + // Note: If the reorged coinbase is re-added to the main chain, the descendants that have not had their + // states change will remain abandoned and will require manual broadcast if the user wants them. RecursiveUpdateTxState(hashTx, try_updating_state); diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index d3d6b85db9f3..a2711fc2c6bc 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -6,7 +6,6 @@ WARNING: This test uses 4GB of disk space. -This test takes 30 mins or more (up to 2 hours) """ import os @@ -96,7 +95,7 @@ def set_test_params(self): ["-disablegovernance","-txindex=0","-maxreceivebuffer=20000","-prune=550"] + DEPLOYMENT_ARGS, ["-disablegovernance","-txindex=0","-maxreceivebuffer=20000","-blockmaxsize=999000"] + DEPLOYMENT_ARGS, ["-disablegovernance","-txindex=0","-maxreceivebuffer=20000","-blockmaxsize=999000"] + DEPLOYMENT_ARGS, - ["-disablegovernance","-txindex=0","-prune=550"] + DEPLOYMENT_ARGS, + ["-disablegovernance","-txindex=0","-prune=550", "-blockfilterindex=1"] + DEPLOYMENT_ARGS, ] self.rpc_timeout = 120 @@ -366,20 +365,20 @@ def has_block(index): self.log.info("Success") - def wallet_test(self): + def test_wallet_rescan(self): # check that the pruning node's wallet is still in good shape self.log.info("Stop and start pruning node to trigger wallet rescan") self.restart_node(2, extra_args=["-disablegovernance", "-txindex=0", "-prune=550"] + DEPLOYMENT_ARGS, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) - self.log.info("Success") + + self.wait_until(lambda: self.nodes[2].getwalletinfo()["scanning"] == False) + self.wait_until(lambda: self.nodes[2].getwalletinfo()["lastprocessedblock"]["height"] == self.nodes[2].getblockcount()) # check that wallet loads successfully when restarting a pruned node after IBD. # this was reported to fail in #7494. - self.log.info("Syncing node 5 to test wallet") - self.connect_nodes(0, 5) - nds = [self.nodes[0], self.nodes[5]] - self.sync_blocks(nds, wait=5, timeout=300) - self.restart_node(5, extra_args=["-disablegovernance", "-txindex=0", "-prune=550"] + DEPLOYMENT_ARGS, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) # restart to trigger rescan - self.log.info("Success") + self.restart_node(5, extra_args=["-disablegovernance", "-txindex=0", "-prune=550", "-blockfilterindex=1"] + DEPLOYMENT_ARGS, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) # restart to trigger rescan + + self.wait_until(lambda: self.nodes[5].getwalletinfo()["scanning"] == False) + self.wait_until(lambda: self.nodes[5].getwalletinfo()["lastprocessedblock"]["height"] == self.nodes[0].getblockcount()) def run_test(self): self.log.info("Warning! This test requires 4GB of disk space") @@ -487,9 +486,13 @@ def run_test(self): self.log.info("Test manual pruning with timestamps") self.manual_test(4, use_timestamp=True) + self.log.info("Syncing node 5 to node 0") + self.connect_nodes(0, 5) + self.sync_blocks([self.nodes[0], self.nodes[5]], wait=5, timeout=300) + if self.is_wallet_compiled(): self.log.info("Test wallet re-scan") - self.wallet_test() + self.test_wallet_rescan() self.log.info("Test it's not possible to rescan beyond pruned data") self.test_rescan_blockchain() @@ -497,6 +500,8 @@ def run_test(self): self.log.info("Test invalid pruning command line options") self.test_invalid_command_line_options() + self.test_scanblocks_pruned() + # NOTE: this is a Dash-specific part, it should be the very last one before "Done" self.log.info("Stopping pruned nodes manually") for i in range(2, 6): @@ -505,5 +510,16 @@ def run_test(self): self.log.info("Done") + def test_scanblocks_pruned(self): + node = self.nodes[5] + genesis_blockhash = node.getblockhash(0) + false_positive_spk = bytes.fromhex("1400000000000000000000000000000000000ad23b") + + assert genesis_blockhash in node.scanblocks( + "start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks'] + + assert_raises_rpc_error(-1, "Block not available (pruned data)", node.scanblocks, + "start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True}) + if __name__ == '__main__': PruneTest().main() diff --git a/test/functional/rpc_scanblocks.py b/test/functional/rpc_scanblocks.py index 594b9d494f41..aa0e0856ece6 100755 --- a/test/functional/rpc_scanblocks.py +++ b/test/functional/rpc_scanblocks.py @@ -61,6 +61,12 @@ def run_test(self): # make sure the blockhash is present when using the first mined block as start_height assert blockhash in node.scanblocks( "start", [f"addr({addr_1})"], height)['relevant_blocks'] + for v in [False, True]: + assert blockhash in node.scanblocks( + action="start", + scanobjects=[f"addr({addr_1})"], + start_height=height, + options={"filter_false_positives": v})['relevant_blocks'] # also test the stop height assert blockhash in node.scanblocks( @@ -93,8 +99,11 @@ def run_test(self): assert genesis_blockhash in node.scanblocks( "start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks'] - # TODO: after an "accurate" mode for scanblocks is implemented (e.g. PR #26325) - # check here that it filters out the false-positive + # check that the filter_false_positives option works + assert genesis_blockhash in node.scanblocks( + "start", [{"desc": f"raw({genesis_coinbase_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks'] + assert genesis_blockhash not in node.scanblocks( + "start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks'] # test node with disabled blockfilterindex assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic", diff --git a/test/functional/wallet_orphanedreward.py b/test/functional/wallet_orphanedreward.py index c31a039d7978..c96d67d84451 100755 --- a/test/functional/wallet_orphanedreward.py +++ b/test/functional/wallet_orphanedreward.py @@ -35,31 +35,44 @@ def run_test(self): # the existing balance and the block reward. self.generate(self.nodes[0], 150) assert_equal(self.nodes[1].getbalance(), Decimal("474.28571429")) + pre_reorg_conf_bals = self.nodes[1].getbalances() txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 30) + orig_chain_tip = self.nodes[0].getbestblockhash() + self.sync_mempools() # Orphan the block reward and make sure that the original coins # from the wallet can still be spent. self.nodes[0].invalidateblock(blk) - self.generate(self.nodes[0], 152) - # Without the following abandontransaction call, the coins are - # not considered available yet. - assert_equal(self.nodes[1].getbalances()["mine"], { - "trusted": Decimal('10.00000000'), - "untrusted_pending": Decimal('0E-8'), - "immature": Decimal('0E-8'), - "coinjoin": Decimal('0E-8'), - }) - # The following abandontransaction is necessary to make the later - # lines succeed, and probably should not be needed; see - # https://github.com/bitcoin/bitcoin/issues/14148. - self.nodes[1].abandontransaction(txid) + blocks = self.generate(self.nodes[0], 152) + conflict_block = blocks[0] + # We expect the descendants of orphaned rewards to no longer be considered assert_equal(self.nodes[1].getbalances()["mine"], { "trusted": Decimal('10.00000000'), "untrusted_pending": Decimal('0.00000000'), "immature": Decimal('0.00000000'), "coinjoin": Decimal('0E-8'), }) - self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 9) + # And the unconfirmed tx to be abandoned + assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True) + + # The abandoning should persist through reloading + self.nodes[1].unloadwallet(self.default_wallet_name) + self.nodes[1].loadwallet(self.default_wallet_name) + assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True) + + # If the orphaned reward is reorged back into the main chain, any unconfirmed + # descendant txs at the time of the original reorg remain abandoned. + self.nodes[0].invalidateblock(conflict_block) + self.nodes[0].reconsiderblock(blk) + assert_equal(self.nodes[0].getbestblockhash(), orig_chain_tip) + self.generate(self.nodes[0], 3) + + balances = self.nodes[1].getbalances() + del balances["lastprocessedblock"] + del pre_reorg_conf_bals["lastprocessedblock"] + assert_equal(balances, pre_reorg_conf_bals) + assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True) + if __name__ == '__main__': OrphanedBlockRewardTest().main() diff --git a/test/functional/wallet_pruning.py b/test/functional/wallet_pruning.py index b688c974fb09..a3d21ccdfe71 100755 --- a/test/functional/wallet_pruning.py +++ b/test/functional/wallet_pruning.py @@ -48,7 +48,7 @@ def skip_test_if_missing_module(self): def mine_large_blocks(self, node, n): # Get the block parameters for the first block - best_block = node.getblock(node.getbestblockhash()) + best_block = node.getblockheader(node.getbestblockhash()) height = int(best_block["height"]) + 1 self.nTime = max(self.nTime, int(best_block["time"])) + 1 previousblockhash = int(best_block["hash"], 16) @@ -132,7 +132,7 @@ def run_test(self): # A blk*.dat file is 128MB # Generate 250 light blocks - self.generate(self.nodes[0], 250, sync_fun=self.no_op) + self.generate(self.nodes[0], 250) # Generate 50MB worth of large blocks in the blk00000.dat file self.mine_large_blocks(self.nodes[0], 50)