mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-01 09:35:52 -05:00
Compare commits
25 commits
93a8f6d2b1
...
13bc8e57ae
Author | SHA1 | Date | |
---|---|---|---|
|
13bc8e57ae | ||
|
85f96b01b7 | ||
|
601a6a6917 | ||
|
eaf4b928e7 | ||
|
992f37f2e1 | ||
|
e1676b08f7 | ||
|
0082f6acc1 | ||
|
79d45b10f1 | ||
|
0713548137 | ||
|
93747d934b | ||
|
0cdddeb224 | ||
|
7fbb1bc44b | ||
|
57ba59c0cd | ||
|
9d2d9f7ce2 | ||
|
595edee169 | ||
|
d73ae603d4 | ||
|
27f99b6d63 | ||
|
42d5d53363 | ||
|
280e65d3a7 | ||
|
8bf2b22fcc | ||
|
9686a9a20e | ||
|
c9417ee593 | ||
|
bfd2fe70ae | ||
|
f3b0593b8b | ||
|
a8bf940ae2 |
27 changed files with 378 additions and 249 deletions
11
doc/release-notes-31600.md
Normal file
11
doc/release-notes-31600.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
Updated RPCs
|
||||
---
|
||||
- the `getblocktemplate` RPC `curtime` (BIP22) and `mintime` (BIP23) fields now
|
||||
account for the timewarp fix proposed in BIP94 on all networks. This ensures
|
||||
that, in the event a timewarp fix softfork activates on mainnet, un-upgraded
|
||||
miners will not accidentally violate the timewarp rule. (#31376, #31600)
|
||||
|
||||
As a reminder, it's important that any software which uses the `getblocktemplate`
|
||||
RPC takes these values into account (either `curtime` or `mintime` is fine).
|
||||
Relying only on a clock can lead to invalid blocks under some circumstances,
|
||||
especially once a timewarp fix is deployed.
|
|
@ -106,6 +106,7 @@ int main(int argc, char* argv[])
|
|||
};
|
||||
auto notifications = std::make_unique<KernelNotifications>();
|
||||
|
||||
kernel::CacheSizes cache_sizes{DEFAULT_KERNEL_CACHE};
|
||||
|
||||
// SETUP: Chainstate
|
||||
auto chainparams = CChainParams::Main();
|
||||
|
@ -119,11 +120,14 @@ int main(int argc, char* argv[])
|
|||
.chainparams = chainman_opts.chainparams,
|
||||
.blocks_dir = abs_datadir / "blocks",
|
||||
.notifications = chainman_opts.notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = abs_datadir / "blocks" / "index",
|
||||
.cache_bytes = cache_sizes.block_tree_db,
|
||||
},
|
||||
};
|
||||
util::SignalInterrupt interrupt;
|
||||
ChainstateManager chainman{interrupt, chainman_opts, blockman_opts};
|
||||
|
||||
kernel::CacheSizes cache_sizes{DEFAULT_KERNEL_CACHE};
|
||||
node::ChainstateLoadOptions options;
|
||||
auto [status, error] = node::LoadChainstate(chainman, cache_sizes, options);
|
||||
if (status != node::ChainstateLoadStatus::SUCCESS) {
|
||||
|
|
20
src/init.cpp
20
src/init.cpp
|
@ -1057,6 +1057,10 @@ bool AppInitParameterInteraction(const ArgsManager& args)
|
|||
.chainparams = chainman_opts_dummy.chainparams,
|
||||
.blocks_dir = args.GetBlocksDirPath(),
|
||||
.notifications = chainman_opts_dummy.notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = 0,
|
||||
},
|
||||
};
|
||||
auto blockman_result{ApplyArgsManOptions(args, blockman_opts_dummy)};
|
||||
if (!blockman_result) {
|
||||
|
@ -1203,18 +1207,33 @@ static ChainstateLoadResult InitAndLoadChainstate(
|
|||
.signals = node.validation_signals.get(),
|
||||
};
|
||||
Assert(ApplyArgsManOptions(args, chainman_opts)); // no error can happen, already checked in AppInitParameterInteraction
|
||||
|
||||
BlockManager::Options blockman_opts{
|
||||
.chainparams = chainman_opts.chainparams,
|
||||
.blocks_dir = args.GetBlocksDirPath(),
|
||||
.notifications = chainman_opts.notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = cache_sizes.block_tree_db,
|
||||
.wipe_data = do_reindex,
|
||||
},
|
||||
};
|
||||
Assert(ApplyArgsManOptions(args, blockman_opts)); // no error can happen, already checked in AppInitParameterInteraction
|
||||
|
||||
// Creating the chainstate manager internally creates a BlockManager, opens
|
||||
// the blocks tree db, and wipes existing block files in case of a reindex.
|
||||
// The coinsdb is opened at a later point on LoadChainstate.
|
||||
try {
|
||||
node.chainman = std::make_unique<ChainstateManager>(*Assert(node.shutdown_signal), chainman_opts, blockman_opts);
|
||||
} catch (dbwrapper_error& e) {
|
||||
LogError("%s", e.what());
|
||||
return {ChainstateLoadStatus::FAILURE, _("Error opening block database")};
|
||||
} catch (std::exception& e) {
|
||||
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated(strprintf("Failed to initialize ChainstateManager: %s", e.what()))};
|
||||
}
|
||||
ChainstateManager& chainman = *node.chainman;
|
||||
if (chainman.m_interrupt) return {ChainstateLoadStatus::INTERRUPTED, {}};
|
||||
|
||||
// This is defined and set here instead of inline in validation.h to avoid a hard
|
||||
// dependency between validation and index/base, since the latter is not in
|
||||
// libbitcoinkernel.
|
||||
|
@ -1237,7 +1256,6 @@ static ChainstateLoadResult InitAndLoadChainstate(
|
|||
};
|
||||
node::ChainstateLoadOptions options;
|
||||
options.mempool = Assert(node.mempool.get());
|
||||
options.wipe_block_tree_db = do_reindex;
|
||||
options.wipe_chainstate_db = do_reindex || do_reindex_chainstate;
|
||||
options.prune = chainman.m_blockman.IsPruneMode();
|
||||
options.check_blocks = args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
|
||||
|
|
|
@ -289,6 +289,9 @@ public:
|
|||
//! Check if any block has been pruned.
|
||||
virtual bool havePruned() = 0;
|
||||
|
||||
//! Get the current prune height.
|
||||
virtual std::optional<int> getPruneHeight() = 0;
|
||||
|
||||
//! Check if the node is ready to broadcast transactions.
|
||||
virtual bool isReadyToBroadcast() = 0;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#ifndef BITCOIN_KERNEL_BLOCKMANAGER_OPTS_H
|
||||
#define BITCOIN_KERNEL_BLOCKMANAGER_OPTS_H
|
||||
|
||||
#include <dbwrapper.h>
|
||||
#include <kernel/notifications_interface.h>
|
||||
#include <util/fs.h>
|
||||
|
||||
|
@ -27,6 +28,7 @@ struct BlockManagerOpts {
|
|||
bool fast_prune{false};
|
||||
const fs::path blocks_dir;
|
||||
Notifications& notifications;
|
||||
DBParams block_tree_db_params;
|
||||
};
|
||||
|
||||
} // namespace kernel
|
||||
|
|
|
@ -42,7 +42,6 @@ struct ChainstateManagerOpts {
|
|||
std::optional<uint256> assumed_valid_block{};
|
||||
//! If the tip is older than this, the node is considered to be in initial block download.
|
||||
std::chrono::seconds max_tip_age{DEFAULT_MAX_TIP_AGE};
|
||||
DBOptions block_tree_db{};
|
||||
DBOptions coins_db{};
|
||||
CoinsViewOptions coins_view{};
|
||||
Notifications& notifications;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <common/args.h>
|
||||
#include <node/blockstorage.h>
|
||||
#include <node/database_args.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/result.h>
|
||||
#include <util/translation.h>
|
||||
|
@ -34,6 +35,8 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, BlockManager::Op
|
|||
|
||||
if (auto value{args.GetBoolArg("-fastprune")}) opts.fast_prune = *value;
|
||||
|
||||
ReadDatabaseArgs(args, opts.block_tree_db_params.options);
|
||||
|
||||
return {};
|
||||
}
|
||||
} // namespace node
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include <util/translation.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
|
@ -1169,7 +1170,19 @@ BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts)
|
|||
m_opts{std::move(opts)},
|
||||
m_block_file_seq{FlatFileSeq{m_opts.blocks_dir, "blk", m_opts.fast_prune ? 0x4000 /* 16kB */ : BLOCKFILE_CHUNK_SIZE}},
|
||||
m_undo_file_seq{FlatFileSeq{m_opts.blocks_dir, "rev", UNDOFILE_CHUNK_SIZE}},
|
||||
m_interrupt{interrupt} {}
|
||||
m_interrupt{interrupt}
|
||||
{
|
||||
m_block_tree_db = std::make_unique<BlockTreeDB>(m_opts.block_tree_db_params);
|
||||
|
||||
if (m_opts.block_tree_db_params.wipe_data) {
|
||||
m_block_tree_db->WriteReindexing(true);
|
||||
m_blockfiles_indexed = false;
|
||||
// If we're reindexing in prune mode, wipe away unusable block files and all undo data files
|
||||
if (m_prune_mode) {
|
||||
CleanupBlockRevFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImportingNow
|
||||
{
|
||||
|
|
|
@ -23,10 +23,7 @@
|
|||
#include <validation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
using kernel::CacheSizes;
|
||||
|
@ -36,34 +33,8 @@ namespace node {
|
|||
// to ChainstateManager::InitializeChainstate().
|
||||
static ChainstateLoadResult CompleteChainstateInitialization(
|
||||
ChainstateManager& chainman,
|
||||
const CacheSizes& cache_sizes,
|
||||
const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
|
||||
{
|
||||
auto& pblocktree{chainman.m_blockman.m_block_tree_db};
|
||||
// new BlockTreeDB tries to delete the existing file, which
|
||||
// fails if it's still open from the previous loop. Close it first:
|
||||
pblocktree.reset();
|
||||
try {
|
||||
pblocktree = std::make_unique<BlockTreeDB>(DBParams{
|
||||
.path = chainman.m_options.datadir / "blocks" / "index",
|
||||
.cache_bytes = cache_sizes.block_tree_db,
|
||||
.memory_only = options.block_tree_db_in_memory,
|
||||
.wipe_data = options.wipe_block_tree_db,
|
||||
.options = chainman.m_options.block_tree_db});
|
||||
} catch (dbwrapper_error& err) {
|
||||
LogError("%s\n", err.what());
|
||||
return {ChainstateLoadStatus::FAILURE, _("Error opening block database")};
|
||||
}
|
||||
|
||||
if (options.wipe_block_tree_db) {
|
||||
pblocktree->WriteReindexing(true);
|
||||
chainman.m_blockman.m_blockfiles_indexed = false;
|
||||
//If we're reindexing in prune mode, wipe away unusable block files and all undo data files
|
||||
if (options.prune) {
|
||||
chainman.m_blockman.CleanupBlockRevFiles();
|
||||
}
|
||||
}
|
||||
|
||||
if (chainman.m_interrupt) return {ChainstateLoadStatus::INTERRUPTED, {}};
|
||||
|
||||
// LoadBlockIndex will load m_have_pruned if we've ever removed a
|
||||
|
@ -155,14 +126,12 @@ static ChainstateLoadResult CompleteChainstateInitialization(
|
|||
}
|
||||
}
|
||||
|
||||
if (!options.wipe_block_tree_db) {
|
||||
auto chainstates{chainman.GetAll()};
|
||||
if (std::any_of(chainstates.begin(), chainstates.end(),
|
||||
[](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
|
||||
return {ChainstateLoadStatus::FAILURE, strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
|
||||
chainman.GetConsensus().SegwitHeight)};
|
||||
};
|
||||
}
|
||||
auto chainstates{chainman.GetAll()};
|
||||
if (std::any_of(chainstates.begin(), chainstates.end(),
|
||||
[](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
|
||||
return {ChainstateLoadStatus::FAILURE, strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
|
||||
chainman.GetConsensus().SegwitHeight)};
|
||||
};
|
||||
|
||||
// Now that chainstates are loaded and we're able to flush to
|
||||
// disk, rebalance the coins caches to desired levels based
|
||||
|
@ -208,7 +177,7 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
|
|||
}
|
||||
}
|
||||
|
||||
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options);
|
||||
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options);
|
||||
if (init_status != ChainstateLoadStatus::SUCCESS) {
|
||||
return {init_status, init_error};
|
||||
}
|
||||
|
@ -244,7 +213,7 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
|
|||
// for the fully validated chainstate.
|
||||
chainman.ActiveChainstate().ClearBlockIndexCandidates();
|
||||
|
||||
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options);
|
||||
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options);
|
||||
if (init_status != ChainstateLoadStatus::SUCCESS) {
|
||||
return {init_status, init_error};
|
||||
}
|
||||
|
|
|
@ -22,12 +22,7 @@ namespace node {
|
|||
|
||||
struct ChainstateLoadOptions {
|
||||
CTxMemPool* mempool{nullptr};
|
||||
bool block_tree_db_in_memory{false};
|
||||
bool coins_db_in_memory{false};
|
||||
// Whether to wipe the block tree database when loading it. If set, this
|
||||
// will also set a reindexing flag so any existing block data files will be
|
||||
// scanned and added to the database.
|
||||
bool wipe_block_tree_db{false};
|
||||
// Whether to wipe the chainstate database when loading it. If set, this
|
||||
// will cause the chainstate database to be rebuilt starting from genesis.
|
||||
bool wipe_chainstate_db{false};
|
||||
|
|
|
@ -49,7 +49,6 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManage
|
|||
|
||||
if (auto value{args.GetIntArg("-maxtipage")}) opts.max_tip_age = std::chrono::seconds{*value};
|
||||
|
||||
ReadDatabaseArgs(args, opts.block_tree_db);
|
||||
ReadDatabaseArgs(args, opts.coins_db);
|
||||
ReadCoinsViewArgs(args, opts.coins_view);
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
#include <policy/settings.h>
|
||||
#include <primitives/block.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <rpc/blockchain.h>
|
||||
#include <rpc/protocol.h>
|
||||
#include <rpc/server.h>
|
||||
#include <support/allocators/secure.h>
|
||||
|
@ -770,6 +771,11 @@ public:
|
|||
LOCK(::cs_main);
|
||||
return chainman().m_blockman.m_have_pruned;
|
||||
}
|
||||
std::optional<int> getPruneHeight() override
|
||||
{
|
||||
LOCK(chainman().GetMutex());
|
||||
return GetPruneHeight(chainman().m_blockman, chainman().ActiveChain());
|
||||
}
|
||||
bool isReadyToBroadcast() override { return !chainman().m_blockman.LoadingBlocks() && !isInitialBlockDownload(); }
|
||||
bool isInitialBlockDownload() override
|
||||
{
|
||||
|
|
|
@ -28,16 +28,25 @@
|
|||
#include <utility>
|
||||
|
||||
namespace node {
|
||||
|
||||
int64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_adjustment_interval)
|
||||
{
|
||||
int64_t min_time{pindexPrev->GetMedianTimePast() + 1};
|
||||
// Height of block to be mined.
|
||||
const int height{pindexPrev->nHeight + 1};
|
||||
// Account for BIP94 timewarp rule on all networks. This makes future
|
||||
// activation safer.
|
||||
if (height % difficulty_adjustment_interval == 0) {
|
||||
min_time = std::max<int64_t>(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP);
|
||||
}
|
||||
return min_time;
|
||||
}
|
||||
|
||||
int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev)
|
||||
{
|
||||
int64_t nOldTime = pblock->nTime;
|
||||
int64_t nNewTime{std::max<int64_t>(pindexPrev->GetMedianTimePast() + 1, TicksSinceEpoch<std::chrono::seconds>(NodeClock::now()))};
|
||||
|
||||
// Height of block to be mined.
|
||||
const int height{pindexPrev->nHeight + 1};
|
||||
if (height % consensusParams.DifficultyAdjustmentInterval() == 0) {
|
||||
nNewTime = std::max<int64_t>(nNewTime, pindexPrev->GetBlockTime() - MAX_TIMEWARP);
|
||||
}
|
||||
int64_t nNewTime{std::max<int64_t>(GetMinimumTime(pindexPrev, consensusParams.DifficultyAdjustmentInterval()),
|
||||
TicksSinceEpoch<std::chrono::seconds>(NodeClock::now()))};
|
||||
|
||||
if (nOldTime < nNewTime) {
|
||||
pblock->nTime = nNewTime;
|
||||
|
|
|
@ -211,6 +211,13 @@ private:
|
|||
void SortForBlock(const CTxMemPool::setEntries& package, std::vector<CTxMemPool::txiter>& sortedEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the minimum time a miner should use in the next block. This always
|
||||
* accounts for the BIP94 timewarp rule, so does not necessarily reflect the
|
||||
* consensus limit.
|
||||
*/
|
||||
int64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_adjustment_interval);
|
||||
|
||||
int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev);
|
||||
|
||||
/** Update an old GenerateCoinbaseCommitment from CreateNewBlock after the block txs have changed */
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
using interfaces::BlockTemplate;
|
||||
using interfaces::Mining;
|
||||
using node::BlockAssembler;
|
||||
using node::GetMinimumTime;
|
||||
using node::NodeContext;
|
||||
using node::RegenerateCommitments;
|
||||
using node::UpdateTime;
|
||||
|
@ -674,7 +675,7 @@ static RPCHelpMan getblocktemplate()
|
|||
{RPCResult::Type::NUM, "coinbasevalue", "maximum allowable input to coinbase transaction, including the generation award and transaction fees (in satoshis)"},
|
||||
{RPCResult::Type::STR, "longpollid", "an id to include with a request to longpoll on an update to this template"},
|
||||
{RPCResult::Type::STR, "target", "The hash target"},
|
||||
{RPCResult::Type::NUM_TIME, "mintime", "The minimum timestamp appropriate for the next block time, expressed in " + UNIX_EPOCH_TIME},
|
||||
{RPCResult::Type::NUM_TIME, "mintime", "The minimum timestamp appropriate for the next block time, expressed in " + UNIX_EPOCH_TIME + ". Adjusted for the proposed BIP94 timewarp rule."},
|
||||
{RPCResult::Type::ARR, "mutable", "list of ways the block template may be changed",
|
||||
{
|
||||
{RPCResult::Type::STR, "value", "A way the block template may be changed, e.g. 'time', 'transactions', 'prevblock'"},
|
||||
|
@ -683,7 +684,7 @@ static RPCHelpMan getblocktemplate()
|
|||
{RPCResult::Type::NUM, "sigoplimit", "limit of sigops in blocks"},
|
||||
{RPCResult::Type::NUM, "sizelimit", "limit of block size"},
|
||||
{RPCResult::Type::NUM, "weightlimit", /*optional=*/true, "limit of block weight"},
|
||||
{RPCResult::Type::NUM_TIME, "curtime", "current timestamp in " + UNIX_EPOCH_TIME},
|
||||
{RPCResult::Type::NUM_TIME, "curtime", "current timestamp in " + UNIX_EPOCH_TIME + ". Adjusted for the proposed BIP94 timewarp rule."},
|
||||
{RPCResult::Type::STR, "bits", "compressed target of next block"},
|
||||
{RPCResult::Type::NUM, "height", "The height of the next block"},
|
||||
{RPCResult::Type::STR_HEX, "signet_challenge", /*optional=*/true, "Only on signet"},
|
||||
|
@ -977,7 +978,7 @@ static RPCHelpMan getblocktemplate()
|
|||
result.pushKV("coinbasevalue", (int64_t)block.vtx[0]->vout[0].nValue);
|
||||
result.pushKV("longpollid", tip.GetHex() + ToString(nTransactionsUpdatedLast));
|
||||
result.pushKV("target", hashTarget.GetHex());
|
||||
result.pushKV("mintime", (int64_t)pindexPrev->GetMedianTimePast()+1);
|
||||
result.pushKV("mintime", GetMinimumTime(pindexPrev, consensusParams.DifficultyAdjustmentInterval()));
|
||||
result.pushKV("mutable", std::move(aMutable));
|
||||
result.pushKV("noncerange", "00000000ffffffff");
|
||||
int64_t nSigOpLimit = MAX_BLOCK_SIGOPS_COST;
|
||||
|
|
|
@ -33,6 +33,10 @@ BOOST_AUTO_TEST_CASE(blockmanager_find_block_pos)
|
|||
.chainparams = *params,
|
||||
.blocks_dir = m_args.GetBlocksDirPath(),
|
||||
.notifications = notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = m_args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = 0,
|
||||
},
|
||||
};
|
||||
BlockManager blockman{*Assert(m_node.shutdown_signal), blockman_opts};
|
||||
// simulate adding a genesis block normally
|
||||
|
@ -140,6 +144,10 @@ BOOST_AUTO_TEST_CASE(blockmanager_flush_block_file)
|
|||
.chainparams = Params(),
|
||||
.blocks_dir = m_args.GetBlocksDirPath(),
|
||||
.notifications = notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = m_args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = 0,
|
||||
},
|
||||
};
|
||||
BlockManager blockman{*Assert(m_node.shutdown_signal), blockman_opts};
|
||||
|
||||
|
|
|
@ -62,7 +62,6 @@
|
|||
#include <stdexcept>
|
||||
|
||||
using namespace util::hex_literals;
|
||||
using kernel::BlockTreeDB;
|
||||
using node::ApplyArgsManOptions;
|
||||
using node::BlockAssembler;
|
||||
using node::BlockManager;
|
||||
|
@ -252,14 +251,14 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
|
|||
.chainparams = chainman_opts.chainparams,
|
||||
.blocks_dir = m_args.GetBlocksDirPath(),
|
||||
.notifications = chainman_opts.notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = m_args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = m_kernel_cache_sizes.block_tree_db,
|
||||
.memory_only = opts.block_tree_db_in_memory,
|
||||
.wipe_data = m_args.GetBoolArg("-reindex", false),
|
||||
},
|
||||
};
|
||||
m_node.chainman = std::make_unique<ChainstateManager>(*Assert(m_node.shutdown_signal), chainman_opts, blockman_opts);
|
||||
LOCK(m_node.chainman->GetMutex());
|
||||
m_node.chainman->m_blockman.m_block_tree_db = std::make_unique<BlockTreeDB>(DBParams{
|
||||
.path = m_args.GetDataDirNet() / "blocks" / "index",
|
||||
.cache_bytes = m_kernel_cache_sizes.block_tree_db,
|
||||
.memory_only = true,
|
||||
});
|
||||
};
|
||||
m_make_chainman();
|
||||
}
|
||||
|
@ -285,9 +284,7 @@ void ChainTestingSetup::LoadVerifyActivateChainstate()
|
|||
auto& chainman{*Assert(m_node.chainman)};
|
||||
node::ChainstateLoadOptions options;
|
||||
options.mempool = Assert(m_node.mempool.get());
|
||||
options.block_tree_db_in_memory = m_block_tree_db_in_memory;
|
||||
options.coins_db_in_memory = m_coins_db_in_memory;
|
||||
options.wipe_block_tree_db = m_args.GetBoolArg("-reindex", false);
|
||||
options.wipe_chainstate_db = m_args.GetBoolArg("-reindex", false) || m_args.GetBoolArg("-reindex-chainstate", false);
|
||||
options.prune = chainman.m_blockman.IsPruneMode();
|
||||
options.check_blocks = m_args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
|
||||
|
|
|
@ -393,6 +393,11 @@ struct SnapshotTestSetup : TestChain100Setup {
|
|||
.chainparams = chainman_opts.chainparams,
|
||||
.blocks_dir = m_args.GetBlocksDirPath(),
|
||||
.notifications = chainman_opts.notifications,
|
||||
.block_tree_db_params = DBParams{
|
||||
.path = chainman.m_options.datadir / "blocks" / "index",
|
||||
.cache_bytes = m_kernel_cache_sizes.block_tree_db,
|
||||
.memory_only = m_block_tree_db_in_memory,
|
||||
},
|
||||
};
|
||||
// For robustness, ensure the old manager is destroyed before creating a
|
||||
// new one.
|
||||
|
|
|
@ -5623,9 +5623,8 @@ double ChainstateManager::GuessVerificationProgress(const CBlockIndex* pindex) c
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
if (!Assume(pindex->m_chain_tx_count > 0)) {
|
||||
LogWarning("Internal bug detected: block %d has unset m_chain_tx_count (%s %s). Please report this issue here: %s\n",
|
||||
pindex->nHeight, CLIENT_NAME, FormatFullVersion(), CLIENT_BUGREPORT);
|
||||
if (pindex->m_chain_tx_count == 0) {
|
||||
LogDebug(BCLog::VALIDATION, "Block %d has unset m_chain_tx_count. Unable to estimate verification progress.\n", pindex->nHeight);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1745,20 +1745,27 @@ RPCHelpMan importdescriptors()
|
|||
if (scanned_time <= GetImportTimestamp(request, now) || results.at(i).exists("error")) {
|
||||
response.push_back(results.at(i));
|
||||
} else {
|
||||
std::string error_msg{strprintf("Rescan failed for descriptor with timestamp %d. There "
|
||||
"was an error reading a block from time %d, which is after or within %d seconds "
|
||||
"of key creation, and could contain transactions pertaining to the desc. As a "
|
||||
"result, transactions and coins using this desc may not appear in the wallet.",
|
||||
GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW)};
|
||||
if (pwallet->chain().havePruned()) {
|
||||
error_msg += strprintf(" This error could be caused by pruning or data corruption "
|
||||
"(see bitcoind log for details) and could be dealt with by downloading and "
|
||||
"rescanning the relevant blocks (see -reindex option and rescanblockchain RPC).");
|
||||
} else if (pwallet->chain().hasAssumedValidChain()) {
|
||||
error_msg += strprintf(" This error is likely caused by an in-progress assumeutxo "
|
||||
"background sync. Check logs or getchainstates RPC for assumeutxo background "
|
||||
"sync progress and try again later.");
|
||||
} else {
|
||||
error_msg += strprintf(" This error could potentially caused by data corruption. If "
|
||||
"the issue persists you may want to reindex (see -reindex option).");
|
||||
}
|
||||
|
||||
UniValue result = UniValue(UniValue::VOBJ);
|
||||
result.pushKV("success", UniValue(false));
|
||||
result.pushKV(
|
||||
"error",
|
||||
JSONRPCError(
|
||||
RPC_MISC_ERROR,
|
||||
strprintf("Rescan failed for descriptor with timestamp %d. There was an error reading a "
|
||||
"block from time %d, which is after or within %d seconds of key creation, and "
|
||||
"could contain transactions pertaining to the desc. As a result, transactions "
|
||||
"and coins using this desc may not appear in the wallet. This error could be "
|
||||
"caused by pruning or data corruption (see bitcoind log for details) and could "
|
||||
"be dealt with by downloading and rescanning the relevant blocks (see -reindex "
|
||||
"option and rescanblockchain RPC).",
|
||||
GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW)));
|
||||
result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, error_msg));
|
||||
response.push_back(std::move(result));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <key_io.h>
|
||||
#include <policy/rbf.h>
|
||||
#include <rpc/util.h>
|
||||
#include <rpc/blockchain.h>
|
||||
#include <util/vector.h>
|
||||
#include <wallet/receive.h>
|
||||
#include <wallet/rpc/util.h>
|
||||
|
@ -909,9 +910,15 @@ RPCHelpMan rescanblockchain()
|
|||
}
|
||||
}
|
||||
|
||||
// We can't rescan beyond non-pruned blocks, stop and throw an error
|
||||
// We can't rescan unavailable blocks, stop and throw an error
|
||||
if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), start_height, stop_height)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
|
||||
if (pwallet->chain().havePruned() && pwallet->chain().getPruneHeight() >= start_height) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
|
||||
}
|
||||
if (pwallet->chain().hasAssumedValidChain()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Failed to rescan unavailable blocks likely due to an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later.");
|
||||
}
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Failed to rescan unavailable blocks, potentially caused by data corruption. If the issue persists you may want to reindex (see -reindex option).");
|
||||
}
|
||||
|
||||
CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
|
||||
|
|
|
@ -10,6 +10,7 @@ target_sources(test_bitcoin
|
|||
wallet_test_fixture.cpp
|
||||
db_tests.cpp
|
||||
coinselector_tests.cpp
|
||||
coinselection_tests.cpp
|
||||
feebumper_tests.cpp
|
||||
group_outputs_tests.cpp
|
||||
init_tests.cpp
|
||||
|
|
195
src/wallet/test/coinselection_tests.cpp
Normal file
195
src/wallet/test/coinselection_tests.cpp
Normal file
|
@ -0,0 +1,195 @@
|
|||
// Copyright (c) 2024 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <consensus/amount.h>
|
||||
#include <policy/policy.h>
|
||||
#include <wallet/coinselection.h>
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
namespace wallet {
|
||||
BOOST_FIXTURE_TEST_SUITE(coinselection_tests, TestingSetup)
|
||||
|
||||
static int next_lock_time = 0;
|
||||
static FastRandomContext default_rand;
|
||||
|
||||
/** Default coin selection parameters (dcsp) allow us to only explicitly set
|
||||
* parameters when a diverging value is relevant in the context of a test. */
|
||||
static CoinSelectionParams init_default_params()
|
||||
{
|
||||
CoinSelectionParams dcsp{
|
||||
/*rng_fast*/default_rand,
|
||||
/*change_output_size=*/31,
|
||||
/*change_spend_size=*/68,
|
||||
/*min_change_target=*/50'000,
|
||||
/*effective_feerate=*/CFeeRate(5000),
|
||||
/*long_term_feerate=*/CFeeRate(10'000),
|
||||
/*discard_feerate=*/CFeeRate(3000),
|
||||
/*tx_noinputs_size=*/11 + 31, //static header size + output size
|
||||
/*avoid_partial=*/false,
|
||||
};
|
||||
dcsp.m_change_fee = /*155 sats=*/dcsp.m_effective_feerate.GetFee(dcsp.change_output_size);
|
||||
dcsp.min_viable_change = /*204 sats=*/dcsp.m_discard_feerate.GetFee(dcsp.change_spend_size);
|
||||
dcsp.m_cost_of_change = /*204 + 155 sats=*/dcsp.min_viable_change + dcsp.m_change_fee;
|
||||
dcsp.m_subtract_fee_outputs = false;
|
||||
return dcsp;
|
||||
}
|
||||
|
||||
static const CoinSelectionParams default_cs_params = init_default_params();
|
||||
|
||||
/** Make one OutputGroup with a single UTXO that either has a given effective value (default) or a given amount (`is_eff_value = false`). */
|
||||
static OutputGroup MakeCoin(const CAmount& amount, bool is_eff_value = true, CoinSelectionParams cs_params = default_cs_params, int custom_spending_vsize = 68)
|
||||
{
|
||||
// Always assume that we only have one input
|
||||
CMutableTransaction tx;
|
||||
tx.vout.resize(1);
|
||||
CAmount fees = cs_params.m_effective_feerate.GetFee(custom_spending_vsize);
|
||||
tx.vout[0].nValue = amount + int(is_eff_value) * fees;
|
||||
tx.nLockTime = next_lock_time++; // so all transactions get different hashes
|
||||
OutputGroup group(cs_params);
|
||||
group.Insert(std::make_shared<COutput>(COutPoint(tx.GetHash(), 0), tx.vout.at(0), /*depth=*/1, /*input_bytes=*/custom_spending_vsize, /*spendable=*/true, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/fees), /*ancestors=*/0, /*descendants=*/0);
|
||||
return group;
|
||||
}
|
||||
|
||||
/** Make multiple OutputGroups with the given values as their effective value */
|
||||
static void AddCoins(std::vector<OutputGroup>& utxo_pool, std::vector<CAmount> coins, CoinSelectionParams cs_params = default_cs_params)
|
||||
{
|
||||
for (CAmount c : coins) {
|
||||
utxo_pool.push_back(MakeCoin(c, true, cs_params));
|
||||
}
|
||||
}
|
||||
|
||||
/** Make multiple coins that share the same effective value */
|
||||
static void AddDuplicateCoins(std::vector<OutputGroup>& utxo_pool, int count, int amount) {
|
||||
for (int i = 0 ; i < count; ++i) {
|
||||
utxo_pool.push_back(MakeCoin(amount));
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if SelectionResult a is equivalent to SelectionResult b.
|
||||
* Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */
|
||||
static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
|
||||
{
|
||||
std::vector<CAmount> a_amts;
|
||||
std::vector<CAmount> b_amts;
|
||||
for (const auto& coin : a.GetInputSet()) {
|
||||
a_amts.push_back(coin->txout.nValue);
|
||||
}
|
||||
for (const auto& coin : b.GetInputSet()) {
|
||||
b_amts.push_back(coin->txout.nValue);
|
||||
}
|
||||
std::sort(a_amts.begin(), a_amts.end());
|
||||
std::sort(b_amts.begin(), b_amts.end());
|
||||
|
||||
std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator> ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin());
|
||||
return ret.first == a_amts.end() && ret.second == b_amts.end();
|
||||
}
|
||||
|
||||
static void TestBnBSuccess(std::string test_title, std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const std::vector<CAmount>& expected_input_amounts, const CoinSelectionParams& cs_params = default_cs_params)
|
||||
{
|
||||
SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB);
|
||||
CAmount expected_amount = 0;
|
||||
for (CAmount input_amount : expected_input_amounts) {
|
||||
OutputGroup group = MakeCoin(input_amount, true, cs_params);
|
||||
expected_amount += group.m_value;
|
||||
expected_result.AddInput(group);
|
||||
}
|
||||
|
||||
const auto result = SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/default_cs_params.m_cost_of_change, /*max_selection_weight=*/MAX_STANDARD_TX_WEIGHT);
|
||||
BOOST_CHECK_MESSAGE(result, "Falsy result in BnB-Success: " + test_title);
|
||||
BOOST_CHECK_MESSAGE(EquivalentResult(expected_result, *result), "Result mismatch in BnB-Success: " + test_title);
|
||||
BOOST_CHECK_MESSAGE(result->GetSelectedValue() == expected_amount, "Selected amount mismatch in BnB-Success: " + test_title);
|
||||
}
|
||||
|
||||
static void TestBnBFail(std::string test_title, std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target)
|
||||
{
|
||||
BOOST_CHECK_MESSAGE(!SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/default_cs_params.m_cost_of_change, /*max_selection_weight=*/MAX_STANDARD_TX_WEIGHT), "BnB-Fail: " + test_title);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(bnb_test)
|
||||
{
|
||||
std::vector<OutputGroup> utxo_pool;
|
||||
|
||||
// Fail for empty UTXO pool
|
||||
TestBnBFail("Empty UTXO pool", utxo_pool, /*selection_target=*/1 * CENT);
|
||||
|
||||
AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT});
|
||||
|
||||
// Simple success cases
|
||||
TestBnBSuccess("Select smallest UTXO", utxo_pool, /*selection_target=*/1 * CENT, /*expected_input_amounts=*/{1 * CENT});
|
||||
TestBnBSuccess("Select middle UTXO", utxo_pool, /*selection_target=*/3 * CENT, /*expected_input_amounts=*/{3 * CENT});
|
||||
TestBnBSuccess("Select biggest UTXO", utxo_pool, /*selection_target=*/5 * CENT, /*expected_input_amounts=*/{5 * CENT});
|
||||
TestBnBSuccess("Select two UTXOs", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT});
|
||||
TestBnBSuccess("Select all UTXOs", utxo_pool, /*selection_target=*/9 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT, 5 * CENT});
|
||||
|
||||
// BnB finds changeless solution while overshooting by up to cost_of_change
|
||||
TestBnBSuccess("Select upper bound", utxo_pool, /*selection_target=*/4 * CENT - default_cs_params.m_cost_of_change, /*expected_input_amounts=*/{1 * CENT, 3 * CENT});
|
||||
|
||||
// BnB fails to find changeless solution when overshooting by cost_of_change + 1 sat
|
||||
TestBnBFail("Overshoot upper bound", utxo_pool, /*selection_target=*/4 * CENT - default_cs_params.m_cost_of_change - 1);
|
||||
|
||||
// Simple cases without BnB solution
|
||||
TestBnBFail("Smallest combination too big", utxo_pool, /*selection_target=*/0.5 * CENT);
|
||||
TestBnBFail("No UTXO combination in target window", utxo_pool, /*selection_target=*/7 * CENT);
|
||||
TestBnBFail("Select more than available", utxo_pool, /*selection_target=*/10 * CENT);
|
||||
|
||||
// Test skipping of equivalent input sets
|
||||
std::vector<OutputGroup> clone_pool;
|
||||
AddCoins(clone_pool, {2 * CENT, 7 * CENT, 7 * CENT});
|
||||
AddDuplicateCoins(clone_pool, 50'000, 5 * CENT);
|
||||
TestBnBSuccess("Skip equivalent input sets", clone_pool, /*selection_target=*/16 * CENT, /*expected_input_amounts=*/{2 * CENT, 7 * CENT, 7 * CENT});
|
||||
|
||||
/* Test BnB attempt limit (`TOTAL_TRIES`)
|
||||
*
|
||||
* Generally, on a diverse UTXO pool BnB will quickly pass over UTXOs bigger than the target and then start
|
||||
* combining small counts of UTXOs that in sum remain under the selection_target+cost_of_change. When there are
|
||||
* multiple UTXOs that have matching amount and cost, combinations with equivalent input sets are skipped. The UTXO
|
||||
* pool for this test is specifically crafted to create as much branching as possible. The selection target is
|
||||
* 8 CENT while all UTXOs are slightly bigger than 1 CENT. The smallest eight are 100,000…100,007 sats, while the larger
|
||||
* ten are 100,009,…,100,018 sats plus cost_of_change.
|
||||
*
|
||||
* Because BnB will only select input sets that fall between selection_target and selection_target + cost_of_change,
|
||||
* and the search traverses the UTXO pool from large amount to small amount, the search will traverse every single
|
||||
* combination of eight inputs, but all except the last will overshoot by more than cost_of_change on the eighth
|
||||
* input. Only the very last combination with the eight smallest UTXOs falls into the target window.
|
||||
*/
|
||||
std::vector<OutputGroup> doppelganger_pool;
|
||||
std::vector<CAmount> doppelgangers;
|
||||
std::vector<CAmount> expected_inputs;
|
||||
for (int i = 0; i < 17; ++i) {
|
||||
if (i < 8) {
|
||||
// The eight smallest UTXOs can be combined to create expected_result
|
||||
doppelgangers.push_back(1 * CENT + i);
|
||||
expected_inputs.push_back(doppelgangers[i]);
|
||||
} else {
|
||||
// Any eight UTXOs including at least one UTXO with the added cost_of_change will exceed target window
|
||||
doppelgangers.push_back(1 * CENT + default_cs_params.m_cost_of_change + i);
|
||||
}
|
||||
}
|
||||
AddCoins(doppelganger_pool, doppelgangers);
|
||||
// Among up to 17 unique UTXOs of similar effective value we will find a solution composed of the eight smallest UTXOs
|
||||
TestBnBSuccess("Combine smallest 8 of 17 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT, /*expected_input_amounts=*/expected_inputs);
|
||||
|
||||
// Starting with 18 unique UTXOs of similar effective value we will not find the solution due to exceeding the attempt limit
|
||||
AddCoins(doppelganger_pool, {1 * CENT + default_cs_params.m_cost_of_change + 17});
|
||||
TestBnBFail("Exhaust looking for smallest 8 of 18 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test)
|
||||
{
|
||||
// Create sets of UTXOs with the same effective amounts at different feerates (but different absolute amounts)
|
||||
std::vector<OutputGroup> low_feerate_pool; // 5 sat/vB (default, and lower than long_term_feerate of 10 sat/vB)
|
||||
AddCoins(low_feerate_pool, {2 * CENT, 3 * CENT, 5 * CENT, 10 * CENT});
|
||||
TestBnBSuccess("Select many inputs at low feerates", low_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{2 * CENT, 3 * CENT, 5 * CENT});
|
||||
|
||||
CoinSelectionParams high_feerate_params = init_default_params();
|
||||
high_feerate_params.m_effective_feerate = CFeeRate{25'000};
|
||||
std::vector<OutputGroup> high_feerate_pool; // 25 sat/vB (greater than long_term_feerate of 10 sat/vB)
|
||||
AddCoins(high_feerate_pool, {2 * CENT, 3 * CENT, 5 * CENT, 10 * CENT}, high_feerate_params);
|
||||
TestBnBSuccess("Select one input at high feerates", high_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{10 * CENT}, high_feerate_params);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
} // namespace wallet
|
|
@ -37,15 +37,6 @@ static const CoinEligibilityFilter filter_confirmed(1, 1, 0);
|
|||
static const CoinEligibilityFilter filter_standard_extra(6, 6, 0);
|
||||
static int nextLockTime = 0;
|
||||
|
||||
static void add_coin(const CAmount& nValue, int nInput, std::vector<COutput>& set)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
tx.vout.resize(nInput + 1);
|
||||
tx.vout[nInput].nValue = nValue;
|
||||
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
|
||||
set.emplace_back(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0);
|
||||
}
|
||||
|
||||
static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
|
@ -133,18 +124,6 @@ static bool EqualResult(const SelectionResult& a, const SelectionResult& b)
|
|||
return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();
|
||||
}
|
||||
|
||||
static CAmount make_hard_case(int utxos, std::vector<COutput>& utxo_pool)
|
||||
{
|
||||
utxo_pool.clear();
|
||||
CAmount target = 0;
|
||||
for (int i = 0; i < utxos; ++i) {
|
||||
target += CAmount{1} << (utxos+i);
|
||||
add_coin(CAmount{1} << (utxos+i), 2*i, utxo_pool);
|
||||
add_coin((CAmount{1} << (utxos+i)) + (CAmount{1} << (utxos-1-i)), 2*i + 1, utxo_pool);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& available_coins, bool subtract_fee_outputs = false)
|
||||
{
|
||||
static std::vector<OutputGroup> static_groups;
|
||||
|
@ -195,115 +174,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
|
|||
std::vector<COutput> utxo_pool;
|
||||
SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB);
|
||||
|
||||
/////////////////////////
|
||||
// Known Outcome tests //
|
||||
/////////////////////////
|
||||
|
||||
// Empty utxo pool
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT));
|
||||
|
||||
// Add utxos
|
||||
add_coin(1 * CENT, 1, utxo_pool);
|
||||
add_coin(2 * CENT, 2, utxo_pool);
|
||||
add_coin(3 * CENT, 3, utxo_pool);
|
||||
add_coin(4 * CENT, 4, utxo_pool);
|
||||
|
||||
// Select 1 Cent
|
||||
add_coin(1 * CENT, 1, expected_result);
|
||||
const auto result1 = SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT);
|
||||
BOOST_CHECK(result1);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result1));
|
||||
BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT);
|
||||
expected_result.Clear();
|
||||
|
||||
// Select 2 Cent
|
||||
add_coin(2 * CENT, 2, expected_result);
|
||||
const auto result2 = SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT);
|
||||
BOOST_CHECK(result2);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result2));
|
||||
BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 2 * CENT);
|
||||
expected_result.Clear();
|
||||
|
||||
// Select 5 Cent
|
||||
add_coin(3 * CENT, 3, expected_result);
|
||||
add_coin(2 * CENT, 2, expected_result);
|
||||
const auto result3 = SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT);
|
||||
BOOST_CHECK(result3);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result3));
|
||||
BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 5 * CENT);
|
||||
expected_result.Clear();
|
||||
|
||||
// Select 11 Cent, not possible
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT));
|
||||
expected_result.Clear();
|
||||
|
||||
// Cost of change is greater than the difference between target value and utxo sum
|
||||
add_coin(1 * CENT, 1, expected_result);
|
||||
const auto result4 = SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT);
|
||||
BOOST_CHECK(result4);
|
||||
BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 1 * CENT);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result4));
|
||||
expected_result.Clear();
|
||||
|
||||
// Cost of change is less than the difference between target value and utxo sum
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0));
|
||||
expected_result.Clear();
|
||||
|
||||
// Select 10 Cent
|
||||
add_coin(5 * CENT, 5, utxo_pool);
|
||||
add_coin(4 * CENT, 4, expected_result);
|
||||
add_coin(3 * CENT, 3, expected_result);
|
||||
add_coin(2 * CENT, 2, expected_result);
|
||||
add_coin(1 * CENT, 1, expected_result);
|
||||
const auto result5 = SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT);
|
||||
BOOST_CHECK(result5);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result5));
|
||||
BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 10 * CENT);
|
||||
expected_result.Clear();
|
||||
|
||||
// Select 0.25 Cent, not possible
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT));
|
||||
expected_result.Clear();
|
||||
|
||||
// Iteration exhaustion test
|
||||
CAmount target = make_hard_case(17, utxo_pool);
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 1)); // Should exhaust
|
||||
target = make_hard_case(14, utxo_pool);
|
||||
const auto result7 = SelectCoinsBnB(GroupCoins(utxo_pool), target, 1); // Should not exhaust
|
||||
BOOST_CHECK(result7);
|
||||
|
||||
// Test same value early bailout optimization
|
||||
utxo_pool.clear();
|
||||
add_coin(7 * CENT, 7, expected_result);
|
||||
add_coin(7 * CENT, 7, expected_result);
|
||||
add_coin(7 * CENT, 7, expected_result);
|
||||
add_coin(7 * CENT, 7, expected_result);
|
||||
add_coin(2 * CENT, 7, expected_result);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(2 * CENT, 7, utxo_pool);
|
||||
for (int i = 0; i < 50000; ++i) {
|
||||
add_coin(5 * CENT, 7, utxo_pool);
|
||||
}
|
||||
const auto result8 = SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000);
|
||||
BOOST_CHECK(result8);
|
||||
BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 30 * CENT);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result8));
|
||||
|
||||
////////////////////
|
||||
// Behavior tests //
|
||||
////////////////////
|
||||
// Select 1 Cent with pool of only greater than 5 Cent
|
||||
utxo_pool.clear();
|
||||
for (int i = 5; i <= 20; ++i) {
|
||||
add_coin(i * CENT, i, utxo_pool);
|
||||
}
|
||||
// Run 100 times, to make sure it is never finding a solution
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT));
|
||||
}
|
||||
|
||||
// Make sure that effective value is working in AttemptSelection when BnB is used
|
||||
CoinSelectionParams coin_selection_params_bnb{
|
||||
|
@ -366,7 +239,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
|
|||
|
||||
CoinsResult available_coins;
|
||||
|
||||
// single coin should be selected when effective fee > long term fee
|
||||
// pre selected coin should be selected even if disadvantageous
|
||||
coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
|
||||
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
|
||||
|
||||
|
@ -377,42 +250,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
|
|||
add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
|
||||
expected_result.Clear();
|
||||
add_coin(10 * CENT + input_fee, 2, expected_result);
|
||||
add_coin(9 * CENT + input_fee, 2, expected_result);
|
||||
add_coin(1 * CENT + input_fee, 2, expected_result);
|
||||
CCoinControl coin_control;
|
||||
const auto result11 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result11));
|
||||
available_coins.Clear();
|
||||
|
||||
// more coins should be selected when effective fee < long term fee
|
||||
coin_selection_params_bnb.m_effective_feerate = CFeeRate(3000);
|
||||
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(5000);
|
||||
|
||||
// Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
|
||||
input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
|
||||
add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
|
||||
expected_result.Clear();
|
||||
add_coin(9 * CENT + input_fee, 2, expected_result);
|
||||
add_coin(1 * CENT + input_fee, 2, expected_result);
|
||||
const auto result12 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
|
||||
BOOST_CHECK(EquivalentResult(expected_result, *result12));
|
||||
available_coins.Clear();
|
||||
|
||||
// pre selected coin should be selected even if disadvantageous
|
||||
coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
|
||||
coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
|
||||
|
||||
// Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
|
||||
input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
|
||||
add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
|
||||
|
||||
expected_result.Clear();
|
||||
add_coin(9 * CENT + input_fee, 2, expected_result);
|
||||
add_coin(1 * CENT + input_fee, 2, expected_result);
|
||||
coin_control.m_allow_other_inputs = true;
|
||||
COutput select_coin = available_coins.All().at(1); // pre select 9 coin
|
||||
coin_control.Select(select_coin.outpoint);
|
||||
|
|
|
@ -153,6 +153,8 @@ class MiningTest(BitcoinTestFramework):
|
|||
# The template will have an adjusted timestamp, which we then modify
|
||||
tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)
|
||||
assert_greater_than_or_equal(tmpl['curtime'], t + MAX_FUTURE_BLOCK_TIME - MAX_TIMEWARP)
|
||||
# mintime and curtime should match
|
||||
assert_equal(tmpl['mintime'], tmpl['curtime'])
|
||||
|
||||
block = CBlock()
|
||||
block.nVersion = tmpl["version"]
|
||||
|
|
|
@ -549,6 +549,7 @@ class BlockchainTest(BitcoinTestFramework):
|
|||
# The chain has probably already been restored by the time reconsiderblock returns,
|
||||
# but poll anyway.
|
||||
self.wait_until(lambda: node.waitfornewblock(timeout=100)['hash'] == current_hash)
|
||||
assert_raises_rpc_error(-1, "Negative timeout", node.waitfornewblock, -1)
|
||||
|
||||
def _test_waitforblockheight(self):
|
||||
self.log.info("Test waitforblockheight")
|
||||
|
|
|
@ -7,11 +7,11 @@ See feature_assumeutxo.py for background.
|
|||
|
||||
## Possible test improvements
|
||||
|
||||
- TODO: test import descriptors while background sync is in progress
|
||||
- TODO: test loading a wallet (backup) on a pruned node
|
||||
|
||||
"""
|
||||
from test_framework.address import address_to_scriptpubkey
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.messages import COIN
|
||||
from test_framework.util import (
|
||||
|
@ -20,6 +20,7 @@ from test_framework.util import (
|
|||
ensure_for,
|
||||
)
|
||||
from test_framework.wallet import MiniWallet
|
||||
from test_framework.wallet_util import get_generate_key
|
||||
|
||||
START_HEIGHT = 199
|
||||
SNAPSHOT_BASE_HEIGHT = 299
|
||||
|
@ -49,6 +50,13 @@ class AssumeutxoTest(BitcoinTestFramework):
|
|||
self.add_nodes(3)
|
||||
self.start_nodes(extra_args=self.extra_args)
|
||||
|
||||
def import_descriptor(self, node, wallet_name, key, timestamp):
|
||||
import_request = [{"desc": descsum_create("pkh(" + key.pubkey + ")"),
|
||||
"timestamp": timestamp,
|
||||
"label": "Descriptor import test"}]
|
||||
wrpc = node.get_wallet_rpc(wallet_name)
|
||||
return wrpc.importdescriptors(import_request)
|
||||
|
||||
def run_test(self):
|
||||
"""
|
||||
Bring up two (disconnected) nodes, mine some new blocks on the first,
|
||||
|
@ -157,6 +165,21 @@ class AssumeutxoTest(BitcoinTestFramework):
|
|||
self.log.info("Backup from before the snapshot height can't be loaded during background sync")
|
||||
assert_raises_rpc_error(-4, "Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height 299", n1.restorewallet, "w2", "backup_w2.dat")
|
||||
|
||||
self.log.info("Test loading descriptors during background sync")
|
||||
wallet_name = "w1"
|
||||
n1.createwallet(wallet_name, disable_private_keys=True)
|
||||
key = get_generate_key()
|
||||
time = n1.getblockchaininfo()['time']
|
||||
timestamp = 0
|
||||
expected_error_message = f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."
|
||||
result = self.import_descriptor(n1, wallet_name, key, timestamp)
|
||||
assert_equal(result[0]['error']['code'], -1)
|
||||
assert_equal(result[0]['error']['message'], expected_error_message)
|
||||
|
||||
self.log.info("Test that rescanning blocks from before the snapshot fails when blocks are not available from the background sync yet")
|
||||
w1 = n1.get_wallet_rpc(wallet_name)
|
||||
assert_raises_rpc_error(-1, "Failed to rescan unavailable blocks likely due to an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later.", w1.rescanblockchain, 100)
|
||||
|
||||
PAUSE_HEIGHT = FINAL_HEIGHT - 40
|
||||
|
||||
self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT)
|
||||
|
@ -204,6 +227,11 @@ class AssumeutxoTest(BitcoinTestFramework):
|
|||
self.wait_until(lambda: len(n2.getchainstates()['chainstates']) == 1)
|
||||
ensure_for(duration=1, f=lambda: (n2.getbalance() == 34))
|
||||
|
||||
self.log.info("Ensuring descriptors can be loaded after background sync")
|
||||
n1.loadwallet(wallet_name)
|
||||
result = self.import_descriptor(n1, wallet_name, key, timestamp)
|
||||
assert_equal(result[0]['success'], True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
AssumeutxoTest(__file__).main()
|
||||
|
|
Loading…
Add table
Reference in a new issue