mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-01 09:35:52 -05:00
Compare commits
26 commits
2d3116cb75
...
8656665641
Author | SHA1 | Date | |
---|---|---|---|
|
8656665641 | ||
|
85f96b01b7 | ||
|
601a6a6917 | ||
|
eaf4b928e7 | ||
|
992f37f2e1 | ||
|
e1676b08f7 | ||
|
0082f6acc1 | ||
|
79d45b10f1 | ||
|
0713548137 | ||
|
93747d934b | ||
|
46f0bc5b48 | ||
|
ce0f154689 | ||
|
02cb132dbe | ||
|
ea3b18c670 | ||
|
e2893c5139 | ||
|
832d468c34 | ||
|
d3600accf9 | ||
|
3db6882017 | ||
|
0cdddeb224 | ||
|
7fbb1bc44b | ||
|
57ba59c0cd | ||
|
9d2d9f7ce2 | ||
|
595edee169 | ||
|
d73ae603d4 | ||
|
27f99b6d63 | ||
|
42d5d53363 |
49 changed files with 340 additions and 143 deletions
7
doc/release-notes-29278.md
Normal file
7
doc/release-notes-29278.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
wallet startup option
|
||||
========================
|
||||
|
||||
- A new wallet startip option `-maxfeerate` is added.
|
||||
- This option sets the upper limit for wallet transaction fee rate.
|
||||
- The wallet will now refrain from creating transactions with a fee rate exceeding the `maxfeerate`.
|
||||
- The default fee rate is 10,000 satoshis per virtual byte.
|
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) {
|
||||
|
|
|
@ -135,7 +135,9 @@ bilingual_str TransactionErrorString(const TransactionError err)
|
|||
case TransactionError::MEMPOOL_ERROR:
|
||||
return Untranslated("Mempool internal error");
|
||||
case TransactionError::MAX_FEE_EXCEEDED:
|
||||
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
|
||||
return Untranslated("Fee exceeds maximum configured by user (maxtxfee)");
|
||||
case TransactionError::MAX_FEE_RATE_EXCEEDED:
|
||||
return Untranslated("Fee rate exceeds maximum configured by user (maxfeerate)");
|
||||
case TransactionError::MAX_BURN_EXCEEDED:
|
||||
return Untranslated("Unspendable output exceeds maximum configured by user (maxburnamount)");
|
||||
case TransactionError::INVALID_PACKAGE:
|
||||
|
|
|
@ -37,6 +37,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
|
|||
"-keypool=<n>",
|
||||
"-maxapsfee=<n>",
|
||||
"-maxtxfee=<amt>",
|
||||
"-maxfeerate=<amt>",
|
||||
"-mintxfee=<amt>",
|
||||
"-paytxfee=<amt>",
|
||||
"-signer=<cmd>",
|
||||
|
|
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);
|
||||
|
|
|
@ -217,6 +217,7 @@ public:
|
|||
//! Return false if the transaction could not be added due to the fee or for another reason.
|
||||
virtual bool broadcastTransaction(const CTransactionRef& tx,
|
||||
const CAmount& max_tx_fee,
|
||||
const CFeeRate& max_tx_fee_rate,
|
||||
bool relay,
|
||||
std::string& err_string) = 0;
|
||||
|
||||
|
@ -289,6 +290,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;
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ public:
|
|||
virtual std::optional<Coin> getUnspentOutput(const COutPoint& output) = 0;
|
||||
|
||||
//! Broadcast transaction.
|
||||
virtual node::TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, std::string& err_string) = 0;
|
||||
virtual node::TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, CFeeRate max_tx_fee_rate, std::string& err_string) = 0;
|
||||
|
||||
//! Get wallet loader.
|
||||
virtual WalletLoader& walletLoader() = 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>
|
||||
|
@ -360,9 +361,9 @@ public:
|
|||
LOCK(::cs_main);
|
||||
return chainman().ActiveChainstate().CoinsTip().GetCoin(output);
|
||||
}
|
||||
TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, std::string& err_string) override
|
||||
TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, CFeeRate max_tx_fee_rate, std::string& err_string) override
|
||||
{
|
||||
return BroadcastTransaction(*m_context, std::move(tx), err_string, max_tx_fee, /*relay=*/ true, /*wait_callback=*/ false);
|
||||
return BroadcastTransaction(*m_context, std::move(tx), err_string, max_tx_fee, max_tx_fee_rate, /*relay=*/true, /*wait_callback=*/false);
|
||||
}
|
||||
WalletLoader& walletLoader() override
|
||||
{
|
||||
|
@ -682,11 +683,12 @@ public:
|
|||
return entry->GetCountWithDescendants() > 1;
|
||||
}
|
||||
bool broadcastTransaction(const CTransactionRef& tx,
|
||||
const CAmount& max_tx_fee,
|
||||
bool relay,
|
||||
std::string& err_string) override
|
||||
const CAmount& max_tx_fee,
|
||||
const CFeeRate& max_tx_fee_rate,
|
||||
bool relay,
|
||||
std::string& err_string) override
|
||||
{
|
||||
const TransactionError err = BroadcastTransaction(m_node, tx, err_string, max_tx_fee, relay, /*wait_callback=*/false);
|
||||
const TransactionError err = BroadcastTransaction(m_node, tx, err_string, max_tx_fee, max_tx_fee_rate, relay, /*wait_callback=*/false);
|
||||
// Chain clients only care about failures to accept the tx to the mempool. Disregard non-mempool related failures.
|
||||
// Note: this will need to be updated if BroadcastTransactions() is updated to return other non-mempool failures
|
||||
// that Chain clients do not need to know about.
|
||||
|
@ -770,6 +772,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 */
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <node/blockstorage.h>
|
||||
#include <node/context.h>
|
||||
#include <node/types.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <txmempool.h>
|
||||
#include <validation.h>
|
||||
#include <validationinterface.h>
|
||||
|
@ -31,7 +32,7 @@ static TransactionError HandleATMPError(const TxValidationState& state, std::str
|
|||
}
|
||||
}
|
||||
|
||||
TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback)
|
||||
TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, const CFeeRate& max_tx_fee_rate, bool relay, bool wait_callback)
|
||||
{
|
||||
// BroadcastTransaction can be called by RPC or by the wallet.
|
||||
// chainman, mempool and peerman are initialized before the RPC server and wallet are started
|
||||
|
@ -69,14 +70,16 @@ TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef t
|
|||
wtxid = mempool_tx->GetWitnessHash();
|
||||
} else {
|
||||
// Transaction is not already in the mempool.
|
||||
if (max_tx_fee > 0) {
|
||||
if (max_tx_fee > 0 || max_tx_fee_rate > CFeeRate(0)) {
|
||||
// First, call ATMP with test_accept and check the fee. If ATMP
|
||||
// fails here, return error immediately.
|
||||
const MempoolAcceptResult result = node.chainman->ProcessTransaction(tx, /*test_accept=*/ true);
|
||||
if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
|
||||
return HandleATMPError(result.m_state, err_string);
|
||||
} else if (result.m_base_fees.value() > max_tx_fee) {
|
||||
} else if (max_tx_fee > 0 && result.m_base_fees.value() > max_tx_fee) {
|
||||
return TransactionError::MAX_FEE_EXCEEDED;
|
||||
} else if (max_tx_fee_rate > CFeeRate(0) && CFeeRate(result.m_base_fees.value(), result.m_vsize.value()) > max_tx_fee_rate) {
|
||||
return TransactionError::MAX_FEE_RATE_EXCEEDED;
|
||||
}
|
||||
}
|
||||
// Try to submit the transaction to the mempool.
|
||||
|
|
|
@ -44,12 +44,13 @@ static const CAmount DEFAULT_MAX_BURN_AMOUNT{0};
|
|||
* @param[in] node reference to node context
|
||||
* @param[in] tx the transaction to broadcast
|
||||
* @param[out] err_string reference to std::string to fill with error string if available
|
||||
* @param[in] max_tx_fee reject txs with fees higher than this (if 0, accept any fee)
|
||||
* @param[in] max_tx_fee reject txs with fees higher than this (if 0, the fee is not checked)
|
||||
* @param[in] max_tx_fee_rate reject txs with fee rate higher than this (if CFeeRate(0), the fee rate is not checked)
|
||||
* @param[in] relay flag if both mempool insertion and p2p relay are requested
|
||||
* @param[in] wait_callback wait until callbacks have been processed to avoid stale result due to a sequentially RPC.
|
||||
* return error
|
||||
*/
|
||||
[[nodiscard]] TransactionError BroadcastTransaction(NodeContext& node, CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback);
|
||||
[[nodiscard]] TransactionError BroadcastTransaction(NodeContext& node, CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, const CFeeRate& max_tx_fee_rate, bool relay, bool wait_callback);
|
||||
|
||||
/**
|
||||
* Return transaction with a given hash.
|
||||
|
|
|
@ -24,6 +24,7 @@ enum class TransactionError {
|
|||
MEMPOOL_REJECTED,
|
||||
MEMPOOL_ERROR,
|
||||
MAX_FEE_EXCEEDED,
|
||||
MAX_FEE_RATE_EXCEEDED,
|
||||
MAX_BURN_EXCEEDED,
|
||||
INVALID_PACKAGE,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <key_io.h>
|
||||
#include <node/psbt.h>
|
||||
#include <node/types.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <policy/policy.h>
|
||||
#include <qt/bitcoinunits.h>
|
||||
#include <qt/forms/ui_psbtoperationsdialog.h>
|
||||
|
@ -120,7 +121,7 @@ void PSBTOperationsDialog::broadcastTransaction()
|
|||
CTransactionRef tx = MakeTransactionRef(mtx);
|
||||
std::string err_string;
|
||||
TransactionError error =
|
||||
m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), err_string);
|
||||
m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), CFeeRate(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK()), err_string);
|
||||
|
||||
if (error == TransactionError::OK) {
|
||||
showStatus(tr("Transaction broadcast successfully! Transaction ID: %1")
|
||||
|
|
|
@ -228,7 +228,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
|
|||
|
||||
// Reject absurdly high fee. (This can never happen because the
|
||||
// wallet never creates transactions with fee greater than
|
||||
// m_default_max_tx_fee. This merely a belt-and-suspenders check).
|
||||
// m_max_tx_fee. This merely a belt-and-suspenders check).
|
||||
if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) {
|
||||
return AbsurdFee;
|
||||
}
|
||||
|
|
|
@ -90,13 +90,11 @@ static RPCHelpMan sendrawtransaction()
|
|||
|
||||
const CFeeRate max_raw_tx_fee_rate{ParseFeeRate(self.Arg<UniValue>("maxfeerate"))};
|
||||
|
||||
int64_t virtual_size = GetVirtualTransactionSize(*tx);
|
||||
CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
|
||||
|
||||
std::string err_string;
|
||||
AssertLockNotHeld(cs_main);
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
const TransactionError err = BroadcastTransaction(node, tx, err_string, max_raw_tx_fee, /*relay=*/true, /*wait_callback=*/true);
|
||||
const TransactionError err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, max_raw_tx_fee_rate, /*relay=*/true, /*wait_callback=*/true);
|
||||
if (TransactionError::OK != err) {
|
||||
throw JSONRPCTransactionError(err, err_string);
|
||||
}
|
||||
|
@ -1060,7 +1058,7 @@ static RPCHelpMan submitpackage()
|
|||
|
||||
// We do not expect an error here; we are only broadcasting things already/still in mempool
|
||||
std::string err_string;
|
||||
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true);
|
||||
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*max_tx_fee_rate=*/CFeeRate(0), /*relay=*/true, /*wait_callback=*/true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err,
|
||||
strprintf("transaction broadcast failed: %s (%d transactions were broadcast successfully)",
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ constexpr TransactionError ALL_TRANSACTION_ERROR[] = {
|
|||
TransactionError::MEMPOOL_REJECTED,
|
||||
TransactionError::MEMPOOL_ERROR,
|
||||
TransactionError::MAX_FEE_EXCEEDED,
|
||||
TransactionError::MAX_FEE_RATE_EXCEEDED,
|
||||
};
|
||||
}; // namespace
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,13 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CMutableTrans
|
|||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
// check that new fee rate does not exceed maxfeerate
|
||||
if (newFeerate > wallet.m_max_tx_fee_rate) {
|
||||
errors.push_back(Untranslated(strprintf("New fee rate %s %s/kvB is too high (cannot be higher than -maxfeerate %s %s/kvB)",
|
||||
FormatMoney(newFeerate.GetFeePerK()), CURRENCY_UNIT, FormatMoney(wallet.m_max_tx_fee_rate.GetFeePerK()), CURRENCY_UNIT)));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
std::vector<COutPoint> reused_inputs;
|
||||
reused_inputs.reserve(mtx.vin.size());
|
||||
for (const CTxIn& txin : mtx.vin) {
|
||||
|
@ -112,7 +119,7 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CMutableTrans
|
|||
}
|
||||
|
||||
// Check that in all cases the new fee doesn't violate maxTxFee
|
||||
const CAmount max_tx_fee = wallet.m_default_max_tx_fee;
|
||||
const CAmount max_tx_fee = wallet.m_max_tx_fee;
|
||||
if (new_total_fee > max_tx_fee) {
|
||||
errors.push_back(Untranslated(strprintf("Specified or calculated fee %s is too high (cannot be higher than -maxtxfee %s)",
|
||||
FormatMoney(new_total_fee), FormatMoney(max_tx_fee))));
|
||||
|
|
|
@ -64,6 +64,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
|
|||
argsman.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in additional (absolute) fees (in %s) if it allows the use of partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-maxtxfee=<amt>", strprintf("Maximum total fees (in %s) to use in a single wallet transaction; setting this too low may abort large transactions (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MAXFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
|
||||
argsman.AddArg("-maxfeerate=<amt>", strprintf("Maximum fee rate (in %s/kvB) for a wallet transactions (default: %s %s/kvB)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_TRANSACTION_FEERATE.GetFeePerK()), CURRENCY_UNIT), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-mintxfee=<amt>", strprintf("Fee rates (in %s/kvB) smaller than this are considered zero fee for transaction creation (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-paytxfee=<amt>", strprintf("Fee rate (in %s/kvB) to add to transactions you send (default: %s)",
|
||||
|
|
|
@ -521,7 +521,7 @@ public:
|
|||
return spk_man != nullptr;
|
||||
}
|
||||
OutputType getDefaultAddressType() override { return m_wallet->m_default_address_type; }
|
||||
CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; }
|
||||
CAmount getDefaultMaxTxFee() override { return m_wallet->m_max_tx_fee; }
|
||||
void remove() override
|
||||
{
|
||||
RemoveWallet(m_context, m_wallet, /*load_on_start=*/false);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -439,15 +439,14 @@ RPCHelpMan settxfee()
|
|||
|
||||
CAmount nAmount = AmountFromValue(request.params[0]);
|
||||
CFeeRate tx_fee_rate(nAmount, 1000);
|
||||
CFeeRate max_tx_fee_rate(pwallet->m_default_max_tx_fee, 1000);
|
||||
if (tx_fee_rate == CFeeRate(0)) {
|
||||
// automatic selection
|
||||
} else if (tx_fee_rate < pwallet->chain().relayMinFee()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be less than min relay tx fee (%s)", pwallet->chain().relayMinFee().ToString()));
|
||||
} else if (tx_fee_rate < pwallet->m_min_fee) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be less than wallet min fee (%s)", pwallet->m_min_fee.ToString()));
|
||||
} else if (tx_fee_rate > max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be more than wallet max tx fee (%s)", max_tx_fee_rate.ToString()));
|
||||
} else if (tx_fee_rate > pwallet->m_max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("fee rate cannot be more than wallet max tx fee rate (%s)", pwallet->m_max_tx_fee_rate.ToString()));
|
||||
}
|
||||
|
||||
pwallet->m_pay_tx_fee = tx_fee_rate;
|
||||
|
@ -1506,9 +1505,12 @@ RPCHelpMan sendall()
|
|||
const std::optional<CAmount> total_bump_fees{pwallet->chain().calculateCombinedBumpFee(outpoints_spent, fee_rate)};
|
||||
CAmount effective_value = total_input_value - fee_from_size - total_bump_fees.value_or(0);
|
||||
|
||||
if (fee_from_size > pwallet->m_default_max_tx_fee) {
|
||||
if (fee_from_size > pwallet->m_max_tx_fee) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED).original);
|
||||
}
|
||||
if (CFeeRate(fee_from_size, tx_size.vsize) > pwallet->m_max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, TransactionErrorString(TransactionError::MAX_FEE_RATE_EXCEEDED).original);
|
||||
}
|
||||
|
||||
if (effective_value <= 0) {
|
||||
if (send_max) {
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -1339,10 +1339,15 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
|
|||
return util::Error{_("Transaction too large")};
|
||||
}
|
||||
|
||||
if (current_fee > wallet.m_default_max_tx_fee) {
|
||||
if (current_fee > wallet.m_max_tx_fee) {
|
||||
return util::Error{TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED)};
|
||||
}
|
||||
|
||||
CFeeRate tx_fee_rate = CFeeRate(current_fee, nBytes);
|
||||
if (tx_fee_rate > wallet.m_max_tx_fee_rate) {
|
||||
return util::Error{TransactionErrorString(TransactionError::MAX_FEE_RATE_EXCEEDED)};
|
||||
}
|
||||
|
||||
if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) {
|
||||
// Lastly, ensure this tx will pass the mempool's chain limits
|
||||
auto result = wallet.chain().checkChainLimits(tx);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <interfaces/chain.h>
|
||||
#include <key_io.h>
|
||||
#include <node/blockstorage.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <policy/policy.h>
|
||||
#include <rpc/server.h>
|
||||
#include <script/solver.h>
|
||||
|
@ -858,7 +859,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
|||
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
auto tx_vsize = GetVirtualTransactionSize(CTransaction{mempool_tx});
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, CFeeRate(tx_vsize, DEFAULT_TRANSACTION_MAXFEE), false, error));
|
||||
|
||||
|
||||
// Reload wallet and make sure new transactions are detected despite events
|
||||
|
@ -900,7 +902,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
|||
block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
auto tx_vsize = GetVirtualTransactionSize(CTransaction{mempool_tx});
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, CFeeRate(tx_vsize, DEFAULT_TRANSACTION_MAXFEE), false, error));
|
||||
m_node.validation_signals->SyncWithValidationInterfaceQueue();
|
||||
});
|
||||
wallet = TestLoadWallet(context);
|
||||
|
|
|
@ -2053,7 +2053,7 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string
|
|||
// If broadcast fails for any reason, trying to set wtx.m_state here would be incorrect.
|
||||
// If transaction was previously in the mempool, it should be updated when
|
||||
// TransactionRemovedFromMempool fires.
|
||||
bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string);
|
||||
bool ret = chain().broadcastTransaction(wtx.tx, m_max_tx_fee, m_max_tx_fee_rate, relay, err_string);
|
||||
if (ret) wtx.m_state = TxStateInMempool{};
|
||||
return ret;
|
||||
}
|
||||
|
@ -3124,17 +3124,17 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
|
|||
}
|
||||
|
||||
if (args.IsArgSet("-maxapsfee")) {
|
||||
const std::string max_aps_fee{args.GetArg("-maxapsfee", "")};
|
||||
if (max_aps_fee == "-1") {
|
||||
const std::string max_aps_fee_str{args.GetArg("-maxapsfee", "")};
|
||||
if (max_aps_fee_str == "-1") {
|
||||
walletInstance->m_max_aps_fee = -1;
|
||||
} else if (std::optional<CAmount> max_fee = ParseMoney(max_aps_fee)) {
|
||||
if (max_fee.value() > HIGH_APS_FEE) {
|
||||
} else if (std::optional<CAmount> max_aps_fee = ParseMoney(max_aps_fee_str)) {
|
||||
if (max_aps_fee.value() > HIGH_APS_FEE) {
|
||||
warnings.push_back(AmountHighWarn("-maxapsfee") + Untranslated(" ") +
|
||||
_("This is the maximum transaction fee you pay (in addition to the normal fee) to prioritize partial spend avoidance over regular coin selection."));
|
||||
}
|
||||
walletInstance->m_max_aps_fee = max_fee.value();
|
||||
walletInstance->m_max_aps_fee = max_aps_fee.value();
|
||||
} else {
|
||||
error = AmountErrMsg("maxapsfee", max_aps_fee);
|
||||
error = AmountErrMsg("maxapsfee", max_aps_fee_str);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -3186,21 +3186,39 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
|
|||
}
|
||||
|
||||
if (args.IsArgSet("-maxtxfee")) {
|
||||
std::optional<CAmount> max_fee = ParseMoney(args.GetArg("-maxtxfee", ""));
|
||||
if (!max_fee) {
|
||||
std::optional<CAmount> max_tx_fee = ParseMoney(args.GetArg("-maxtxfee", ""));
|
||||
if (!max_tx_fee) {
|
||||
error = AmountErrMsg("maxtxfee", args.GetArg("-maxtxfee", ""));
|
||||
return nullptr;
|
||||
} else if (max_fee.value() > HIGH_MAX_TX_FEE) {
|
||||
} else if (max_tx_fee.value() > HIGH_MAX_TX_FEE) {
|
||||
warnings.push_back(strprintf(_("%s is set very high! Fees this large could be paid on a single transaction."), "-maxtxfee"));
|
||||
|
||||
// Wallet prevents creating transactions with fee rates lower than minrelaytxfee.
|
||||
// Also the wallet prevents creating transaction with base fee above maxtxfee.
|
||||
// Warn when a 1kvb transaction, with a base fee set to maxtxfee, has a fee rate less than minrelaytxfee.
|
||||
// It is likely that some transactions with fee rates greater than or equal to the minrelaytxfee will exceed maxtxfee.
|
||||
// In such cases, the wallet won't be able to create transactions. Therefore, warn the user.
|
||||
} else if (chain && CFeeRate(max_tx_fee.value(), 1000) < chain->relayMinFee()) {
|
||||
warnings.push_back(strprintf(_("Invalid amount for %s=<amount>: '%s' conflicts with the minimum relay transaction feerate %s. Please set a higher %s or lower %s"),
|
||||
"-maxtxfee", args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString(), "-maxtxfee", "-minrelaytxfee"));
|
||||
}
|
||||
|
||||
if (chain && CFeeRate{max_fee.value(), 1000} < chain->relayMinFee()) {
|
||||
walletInstance->m_max_tx_fee = max_tx_fee.value();
|
||||
}
|
||||
|
||||
if (args.IsArgSet("-maxfeerate")) {
|
||||
std::optional<CAmount> max_tx_fee_rate = ParseMoney(args.GetArg("-maxfeerate", ""));
|
||||
if (!max_tx_fee_rate) {
|
||||
error = AmountErrMsg("maxfeerate", args.GetArg("-maxfeerate", ""));
|
||||
return nullptr;
|
||||
}
|
||||
if (chain && CFeeRate(max_tx_fee_rate.value()) < chain->relayMinFee()) {
|
||||
error = strprintf(_("Invalid amount for %s=<amount>: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"),
|
||||
"-maxtxfee", args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString());
|
||||
"-maxfeerate", args.GetArg("-maxfeerate", ""), chain->relayMinFee().ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
walletInstance->m_default_max_tx_fee = max_fee.value();
|
||||
walletInstance->m_max_tx_fee_rate = CFeeRate(max_tx_fee_rate.value());
|
||||
}
|
||||
|
||||
if (args.IsArgSet("-consolidatefeerate")) {
|
||||
|
|
|
@ -135,6 +135,8 @@ static const bool DEFAULT_DISABLE_WALLET = false;
|
|||
static const bool DEFAULT_WALLETCROSSCHAIN = false;
|
||||
//! -maxtxfee default
|
||||
constexpr CAmount DEFAULT_TRANSACTION_MAXFEE{COIN / 10};
|
||||
//! maxfeerate default
|
||||
const CFeeRate DEFAULT_MAX_TRANSACTION_FEERATE(CFeeRate(COIN / 10));
|
||||
//! Discourage users to set fees higher than this amount (in satoshis) per kB
|
||||
constexpr CAmount HIGH_TX_FEE_PER_KB{COIN / 100};
|
||||
//! -maxtxfee will warn if called with a higher fee than this amount (in satoshis)
|
||||
|
@ -728,8 +730,10 @@ public:
|
|||
*/
|
||||
std::optional<OutputType> m_default_change_type{};
|
||||
/** Absolute maximum transaction fee (in satoshis) used by default for the wallet */
|
||||
CAmount m_default_max_tx_fee{DEFAULT_TRANSACTION_MAXFEE};
|
||||
CAmount m_max_tx_fee{DEFAULT_TRANSACTION_MAXFEE};
|
||||
|
||||
/** Maximum transaction fee rate used for the wallet */
|
||||
CFeeRate m_max_tx_fee_rate{DEFAULT_MAX_TRANSACTION_FEERATE};
|
||||
/** Number of pre-generated keys/scripts by each spkm (part of the look-ahead process, used to detect payments) */
|
||||
int64_t m_keypool_size{DEFAULT_KEYPOOL_SIZE};
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -387,7 +387,7 @@ class PSBTTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
for param, value in {("fee_rate", 100000), ("feeRate", 1)}:
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (maxtxfee)",
|
||||
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: value, "add_inputs": True})
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: -1, "add_inputs": True})
|
||||
|
@ -441,7 +441,7 @@ class PSBTTest(BitcoinTestFramework):
|
|||
self.log.info("Test walletcreatefundedpsbt with too-high fee rate produces total fee well above -maxtxfee and raises RPC error")
|
||||
# previously this was silently capped at -maxtxfee
|
||||
for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items():
|
||||
msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||
msg = "Fee exceeds maximum configured by user (maxtxfee)"
|
||||
assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"fee_rate": 1000000, "add_inputs": bool_add})
|
||||
assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add})
|
||||
|
||||
|
|
|
@ -400,7 +400,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
|
||||
def sendrawtransaction_testmempoolaccept_tests(self):
|
||||
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
|
||||
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||
fee_rate_exceeds_max = "Fee rate exceeds maximum configured by user (maxfeerate)"
|
||||
|
||||
# Test a transaction with a small fee.
|
||||
# Fee rate is 0.00100000 BTC/kvB
|
||||
|
@ -410,7 +410,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
assert_equal(testres['allowed'], False)
|
||||
assert_equal(testres['reject-reason'], 'max-fee-exceeded')
|
||||
# and sendrawtransaction should throw
|
||||
assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'], 0.00001000)
|
||||
assert_raises_rpc_error(-25, fee_rate_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'], 0.00001000)
|
||||
# and the following calls should both succeed
|
||||
testres = self.nodes[2].testmempoolaccept(rawtxs=[tx['hex']])[0]
|
||||
assert_equal(testres['allowed'], True)
|
||||
|
@ -424,7 +424,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
assert_equal(testres['allowed'], False)
|
||||
assert_equal(testres['reject-reason'], 'max-fee-exceeded')
|
||||
# and sendrawtransaction should throw
|
||||
assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'])
|
||||
assert_raises_rpc_error(-25, fee_rate_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'])
|
||||
# and the following calls should both succeed
|
||||
testres = self.nodes[2].testmempoolaccept(rawtxs=[tx['hex']], maxfeerate='0.20000000')[0]
|
||||
assert_equal(testres['allowed'], True)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -132,8 +132,11 @@ class BumpFeeTest(BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141", rbf_node.bumpfee, rbfid, fee_rate=INSUFFICIENT)
|
||||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10",
|
||||
|
||||
# Bumping to a very high fee rate above the default -maxfeerate should fail
|
||||
assert_raises_rpc_error(-4, "New fee rate 1.00 BTC/kvB is too high (cannot be higher than -maxfeerate 0.10 BTC/kvB)",
|
||||
rbf_node.bumpfee, rbfid, fee_rate=TOO_HIGH)
|
||||
|
||||
# Test fee_rate with zero values.
|
||||
msg = "Insufficient total fee 0.00"
|
||||
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
|
||||
|
@ -547,9 +550,9 @@ def test_settxfee(self, rbf_node, dest_address):
|
|||
assert_greater_than(Decimal("0.00001000"), abs(requested_feerate - actual_feerate))
|
||||
rbf_node.settxfee(Decimal("0.00000000")) # unset paytxfee
|
||||
|
||||
# check that settxfee respects -maxtxfee
|
||||
self.restart_node(1, ['-maxtxfee=0.000025'] + self.extra_args[1])
|
||||
assert_raises_rpc_error(-8, "txfee cannot be more than wallet max tx fee", rbf_node.settxfee, Decimal('0.00003'))
|
||||
# check that settxfee respects -maxfeerate
|
||||
self.restart_node(1, ['-maxfeerate=0.000025'] + self.extra_args[1])
|
||||
assert_raises_rpc_error(-8, "fee rate cannot be more than wallet max tx fee rate", rbf_node.settxfee, Decimal('0.00003'))
|
||||
self.restart_node(1, self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
self.connect_nodes(1, 0)
|
||||
|
@ -564,7 +567,24 @@ def test_maxtxfee_fails(self, rbf_node, dest_address):
|
|||
self.restart_node(1, ['-maxtxfee=0.000025'] + self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
rbfid = spend_one_input(rbf_node, dest_address)
|
||||
assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", rbf_node.bumpfee, rbfid)
|
||||
assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by user (maxtxfee)", rbf_node.bumpfee, rbfid)
|
||||
|
||||
# When user passed fee rate causes base fee to be above maxtxfee we fail early
|
||||
assert_raises_rpc_error(-4, "Specified or calculated fee 0.0000282 is too high (cannot be higher than -maxtxfee 0.000025)", rbf_node.bumpfee, rbfid, fee_rate=20)
|
||||
|
||||
self.log.info("Test that a low -maxtxfee, which may prevent tx fee rate from reaching -minrelaytxfee triggers a warning.")
|
||||
low_max_tx_fee = '0.000001'
|
||||
high_max_tx_fee = '0.001'
|
||||
high_min_relay_fee = '0.0002'
|
||||
msg = f"Invalid amount for -maxtxfee=<amount>: '{low_max_tx_fee}' conflicts with the minimum relay transaction feerate {format(float(high_min_relay_fee), '.8f')} BTC/kvB. Please set a higher -maxtxfee or lower -minrelaytxfee"
|
||||
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_min_relay_fee}', f'-maxtxfee={low_max_tx_fee}'])
|
||||
warnings = self.nodes[1].createwallet("test-wallet")["warnings"]
|
||||
assert msg in warnings
|
||||
|
||||
self.log.info("Test that a -maxtxfee high enough to allow tx fee rate to meet or exceed -minrelaytxfee should start normally.")
|
||||
msg = "Warning: " + msg
|
||||
self.stop_node(1, expected_stderr=msg)
|
||||
self.start_node(1, extra_args=[f'-minrelaytxfee={high_min_relay_fee}', f'-maxtxfee={high_max_tx_fee}'])
|
||||
self.restart_node(1, self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
self.connect_nodes(1, 0)
|
||||
|
|
|
@ -61,12 +61,12 @@ class CreateTxWalletTest(BitcoinTestFramework):
|
|||
self.restart_node(0, extra_args=[fee_setting])
|
||||
assert_raises_rpc_error(
|
||||
-6,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].sendmany(dummy="", amounts=outputs),
|
||||
)
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx),
|
||||
)
|
||||
|
||||
|
@ -75,12 +75,12 @@ class CreateTxWalletTest(BitcoinTestFramework):
|
|||
self.nodes[0].settxfee(0.01)
|
||||
assert_raises_rpc_error(
|
||||
-6,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].sendmany(dummy="", amounts=outputs),
|
||||
)
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx),
|
||||
)
|
||||
self.nodes[0].settxfee(0)
|
||||
|
|
|
@ -853,7 +853,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
for param, value in {("fee_rate", 100000), ("feeRate", 1.000)}:
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (maxtxfee)",
|
||||
node.fundrawtransaction, rawtx, add_inputs=True, **{param: value})
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
node.fundrawtransaction, rawtx, add_inputs=True, **{param: -1})
|
||||
|
|
|
@ -184,6 +184,30 @@ class WalletSendTest(BitcoinTestFramework):
|
|||
|
||||
return res
|
||||
|
||||
def test_maxfeerate(self):
|
||||
self.log.info("test -maxfeerate enforcement on wallet transactions.")
|
||||
# Default maxfeerate is 10,000 sat/vB
|
||||
# Wallet will reject all transactions with fee rate above 10,000 sat/vB.
|
||||
assert_raises_rpc_error(-6, "Fee rate exceeds maximum configured by user (maxfeerate)",
|
||||
self.nodes[0].sendtoaddress, address=self.nodes[0].getnewaddress(), amount=1, fee_rate=10001)
|
||||
|
||||
# All transaction with fee rate <= 10,000 sat/vB can be created.
|
||||
self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=9900)
|
||||
|
||||
# Configure a lower -maxfeerate of 10 sat/vB.
|
||||
self.restart_node(0, extra_args=['-maxfeerate=0.00010'])
|
||||
|
||||
# Wallet will reject all transactions with fee rate above 10 sat/vB.
|
||||
assert_raises_rpc_error(-6, "Fee rate exceeds maximum configured by user (maxfeerate)",
|
||||
self.nodes[0].sendtoaddress, address=self.nodes[0].getnewaddress(), amount=1, fee_rate=11)
|
||||
|
||||
# Fee rates <= 10 sat/vB will be accepted by the wallet.
|
||||
self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=9)
|
||||
|
||||
# Restart the node with the default -maxfeerate option
|
||||
self.restart_node(0)
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("Setup wallets...")
|
||||
# w0 is a wallet with coinbase rewards
|
||||
|
@ -580,6 +604,9 @@ class WalletSendTest(BitcoinTestFramework):
|
|||
# Check tx creation size limits
|
||||
self.test_weight_limits()
|
||||
|
||||
self.test_maxfeerate()
|
||||
|
||||
|
||||
def test_weight_limits(self):
|
||||
self.log.info("Test weight limits")
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue