diff --git a/doc/bips.md b/doc/bips.md index 07e5024864c6..ebf6b8fcd7d7 100644 --- a/doc/bips.md +++ b/doc/bips.md @@ -49,6 +49,7 @@ BIPs that are implemented by Bitcoin Core: * [`BIP 173`](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki): Bech32 addresses for native Segregated Witness outputs are supported as of **v0.16.0** ([PR 11167](https://github.com/bitcoin/bitcoin/pull/11167)). Bech32 addresses are generated by default as of **v0.20.0** ([PR 16884](https://github.com/bitcoin/bitcoin/pull/16884)). * [`BIP 174`](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki): RPCs to operate on Partially Signed Bitcoin Transactions (PSBT) are present as of **v0.17.0** ([PR 13557](https://github.com/bitcoin/bitcoin/pull/13557)). * [`BIP 176`](https://github.com/bitcoin/bips/blob/master/bip-0176.mediawiki): Bits Denomination [QT only] is supported as of **v0.16.0** ([PR 12035](https://github.com/bitcoin/bitcoin/pull/12035)). +* [`BIP 323`](https://github.com/bitcoin/bips/blob/master/bip-0323.mediawiki): BIP 9 bits 5 to 28 (inclusive) are ignored for soft-fork signalling and unknown soft fork warnings as of **v32.0**. * [`BIP 324`](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki): The v2 transport protocol specified by BIP324 and the associated `NODE_P2P_V2` service bit are supported as of **v26.0**, but off by default ([PR 28331](https://github.com/bitcoin/bitcoin/pull/28331)). On by default as of **v27.0** ([PR 29347](https://github.com/bitcoin/bitcoin/pull/29347)). * [`BIP 325`](https://github.com/bitcoin/bips/blob/master/bip-0325.mediawiki): Signet test network is supported as of **v0.21.0** ([PR 18267](https://github.com/bitcoin/bitcoin/pull/18267)). * [`BIP 327`](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki): Key aggregation via `musig()` descriptors is supported as of **v30.0** ([PR 31244](https://github.com/bitcoin/bitcoin/pull/31244)). Signing is supported as of **v31.0** ([PR 29675](https://github.com/bitcoin/bitcoin/pull/29675)) diff --git a/doc/release-notes-34779.md b/doc/release-notes-34779.md new file mode 100644 index 000000000000..7f1f5099d064 --- /dev/null +++ b/doc/release-notes-34779.md @@ -0,0 +1,5 @@ +Logging +------- + +- BIP 9 bits 5 to 28 inclusive are now ignored for soft fork signaling, as per BIP 323. We won't + warn about unknown deployments when receiving blocks that set any of those bits in their version. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 290b54261f03..bab438484144 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -23,29 +23,8 @@ using util::SplitString; -void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& options) -{ - if (!args.GetArgs("-signetseednode").empty()) { - options.seeds.emplace(args.GetArgs("-signetseednode")); - } - if (!args.GetArgs("-signetchallenge").empty()) { - const auto signet_challenge = args.GetArgs("-signetchallenge"); - if (signet_challenge.size() != 1) { - throw std::runtime_error("-signetchallenge cannot be multiple values."); - } - const auto val{TryParseHex(signet_challenge[0])}; - if (!val) { - throw std::runtime_error(strprintf("-signetchallenge must be hex, not '%s'.", signet_challenge[0])); - } - options.challenge.emplace(*val); - } -} - -void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& options) +static void HandleDeploymentArgs(const ArgsManager& args, CChainParams::DeploymentOptions& options) { - if (auto value = args.GetBoolArg("-fastprune")) options.fastprune = *value; - if (HasTestOption(args, "bip94")) options.enforce_bip94 = true; - for (const std::string& arg : args.GetArgs("-testactivationheight")) { const auto found{arg.find('@')}; if (found == std::string::npos) { @@ -107,6 +86,43 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti } } +void ReadMainNetArgs(const ArgsManager& args, CChainParams::MainNetOptions& options) +{ + HandleDeploymentArgs(args, options.dep_opts); +} + +void ReadTestNetArgs(const ArgsManager& args, CChainParams::TestNetOptions& options) +{ + HandleDeploymentArgs(args, options.dep_opts); +} + +void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& options) +{ + if (!args.GetArgs("-signetseednode").empty()) { + options.seeds.emplace(args.GetArgs("-signetseednode")); + } + if (!args.GetArgs("-signetchallenge").empty()) { + const auto signet_challenge = args.GetArgs("-signetchallenge"); + if (signet_challenge.size() != 1) { + throw std::runtime_error("-signetchallenge cannot be multiple values."); + } + const auto val{TryParseHex(signet_challenge[0])}; + if (!val) { + throw std::runtime_error(strprintf("-signetchallenge must be hex, not '%s'.", signet_challenge[0])); + } + options.challenge.emplace(*val); + } + HandleDeploymentArgs(args, options.dep_opts); +} + +void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& options) +{ + if (auto value = args.GetBoolArg("-fastprune")) options.fastprune = *value; + if (HasTestOption(args, "bip94")) options.enforce_bip94 = true; + + HandleDeploymentArgs(args, options.dep_opts); +} + static std::unique_ptr globalChainParams; const CChainParams &Params() { @@ -117,12 +133,21 @@ const CChainParams &Params() { std::unique_ptr CreateChainParams(const ArgsManager& args, const ChainType chain) { switch (chain) { - case ChainType::MAIN: - return CChainParams::Main(); - case ChainType::TESTNET: - return CChainParams::TestNet(); - case ChainType::TESTNET4: - return CChainParams::TestNet4(); + case ChainType::MAIN: { + auto opts = CChainParams::MainNetOptions{}; + ReadMainNetArgs(args, opts); + return CChainParams::Main(opts); + } + case ChainType::TESTNET: { + auto opts = CChainParams::TestNetOptions{}; + ReadTestNetArgs(args, opts); + return CChainParams::TestNet(opts); + } + case ChainType::TESTNET4: { + auto opts = CChainParams::TestNetOptions{}; + ReadTestNetArgs(args, opts); + return CChainParams::TestNet4(opts); + } case ChainType::SIGNET: { auto opts = CChainParams::SigNetOptions{}; ReadSigNetArgs(args, opts); diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index d816d1af91c3..f62e455fac45 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -16,10 +16,10 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman) argsman.AddArg("-chain=", "Use the chain (default: main). Allowed values: " LIST_CHAIN_NAMES, ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-regtest", "Enter regression test mode, which uses a special chain in which blocks can be solved instantly. " "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); - argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv). (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv). (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-testnet", "Use the testnet3 chain. Equivalent to -chain=test. Support for testnet3 is deprecated and will be removed in an upcoming release. Consider moving to testnet4 now by using -testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); - argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signet", "Use the signet chain. Equivalent to -chain=signet. Note that the network is defined by the -signetchallenge parameter", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS); argsman.AddArg("-signetseednode", "Specify a seed node for the signet network, in the hostname[:port] format, e.g. sig.net:1234 (may be used multiple times to specify multiple seed nodes; defaults to the global default signet test network seed node(s))", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS); diff --git a/src/init.cpp b/src/init.cpp index 5b823a376f7e..da58efe74c42 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1125,6 +1125,16 @@ bool AppInitParameterInteraction(const ArgsManager& args) } } + // Prevent setting deployment parameters on mainnet. + if (chainparams.GetChainType() == ChainType::MAIN) { + if (args.IsArgSet("-testactivationheight")) { + return InitError(_("The -testactivationheight option may not be used on mainnet.")); + } + if (args.IsArgSet("-vbparams")) { + return InitError(_("The -vbparams option may not be used on mainnet.")); + } + } + // Also report errors from parsing before daemonization { kernel::Notifications notifications{}; diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index 28dad6eb9276..ba3a57d44225 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -813,10 +813,10 @@ btck_ChainParameters* btck_chain_parameters_create(const btck_ChainType chain_ty return btck_ChainParameters::ref(const_cast(CChainParams::TestNet4().release())); } case btck_ChainType_SIGNET: { - return btck_ChainParameters::ref(const_cast(CChainParams::SigNet({}).release())); + return btck_ChainParameters::ref(const_cast(CChainParams::SigNet().release())); } case btck_ChainType_REGTEST: { - return btck_ChainParameters::ref(const_cast(CChainParams::RegTest({}).release())); + return btck_ChainParameters::ref(const_cast(CChainParams::RegTest().release())); } } assert(false); diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 240c617894c8..cc77a373481b 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -73,12 +73,41 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits return CreateGenesisBlock(pszTimestamp, genesisOutputScript, nTime, nNonce, nBits, nVersion, genesisReward); } +void CChainParams::ApplyDeploymentOptions(const DeploymentOptions& opts) +{ + for (const auto& [dep, height] : opts.activation_heights) { + switch (dep) { + case Consensus::BuriedDeployment::DEPLOYMENT_SEGWIT: + consensus.SegwitHeight = int{height}; + break; + case Consensus::BuriedDeployment::DEPLOYMENT_HEIGHTINCB: + consensus.BIP34Height = int{height}; + break; + case Consensus::BuriedDeployment::DEPLOYMENT_DERSIG: + consensus.BIP66Height = int{height}; + break; + case Consensus::BuriedDeployment::DEPLOYMENT_CLTV: + consensus.BIP65Height = int{height}; + break; + case Consensus::BuriedDeployment::DEPLOYMENT_CSV: + consensus.CSVHeight = int{height}; + break; + } + } + + for (const auto& [deployment_pos, version_bits_params] : opts.version_bits_parameters) { + consensus.vDeployments[deployment_pos].nStartTime = version_bits_params.start_time; + consensus.vDeployments[deployment_pos].nTimeout = version_bits_params.timeout; + consensus.vDeployments[deployment_pos].min_activation_height = version_bits_params.min_activation_height; + } +} + /** * Main network on which people trade goods and services. */ class CMainParams : public CChainParams { public: - CMainParams() { + CMainParams(const MainNetOptions& opts) { m_chain_type = ChainType::MAIN; consensus.signet_blocks = false; consensus.signet_challenge.clear(); @@ -107,6 +136,8 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1815; // 90% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + ApplyDeploymentOptions(opts.dep_opts); + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000001128750f82f4c366153a3a030"}; consensus.defaultAssumeValid = uint256{"00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac"}; // 938343 @@ -203,7 +234,7 @@ class CMainParams : public CChainParams { */ class CTestNetParams : public CChainParams { public: - CTestNetParams() { + CTestNetParams(const TestNetOptions& opts) { m_chain_type = ChainType::TESTNET; consensus.signet_blocks = false; consensus.signet_challenge.clear(); @@ -230,6 +261,8 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1512; // 75% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + ApplyDeploymentOptions(opts.dep_opts); + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000017dde1c649f3708d14b6"}; consensus.defaultAssumeValid = uint256{"000000007a61e4230b28ac5cb6b5e5a0130de37ac1faf2f8987d2fa6505b67f4"}; // 4842348 @@ -304,7 +337,7 @@ class CTestNetParams : public CChainParams { */ class CTestNet4Params : public CChainParams { public: - CTestNet4Params() { + CTestNet4Params(const TestNetOptions& opts) { m_chain_type = ChainType::TESTNET4; consensus.signet_blocks = false; consensus.signet_challenge.clear(); @@ -330,6 +363,8 @@ class CTestNet4Params : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1512; // 75% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + ApplyDeploymentOptions(opts.dep_opts); + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000009a0fe15d0177d086304"}; consensus.defaultAssumeValid = uint256{"0000000002368b1e4ee27e2e85676ae6f9f9e69579b29093e9a82c170bf7cf8a"}; // 123613 @@ -473,6 +508,8 @@ class SigNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1815; // 90% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + ApplyDeploymentOptions(options.dep_opts); + // message start is defined as the first 4 bytes of the sha256d of the block script HashWriter h{}; h << consensus.signet_challenge; @@ -567,31 +604,7 @@ class CRegTestParams : public CChainParams m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; - for (const auto& [dep, height] : opts.activation_heights) { - switch (dep) { - case Consensus::BuriedDeployment::DEPLOYMENT_SEGWIT: - consensus.SegwitHeight = int{height}; - break; - case Consensus::BuriedDeployment::DEPLOYMENT_HEIGHTINCB: - consensus.BIP34Height = int{height}; - break; - case Consensus::BuriedDeployment::DEPLOYMENT_DERSIG: - consensus.BIP66Height = int{height}; - break; - case Consensus::BuriedDeployment::DEPLOYMENT_CLTV: - consensus.BIP65Height = int{height}; - break; - case Consensus::BuriedDeployment::DEPLOYMENT_CSV: - consensus.CSVHeight = int{height}; - break; - } - } - - for (const auto& [deployment_pos, version_bits_params] : opts.version_bits_parameters) { - consensus.vDeployments[deployment_pos].nStartTime = version_bits_params.start_time; - consensus.vDeployments[deployment_pos].nTimeout = version_bits_params.timeout; - consensus.vDeployments[deployment_pos].min_activation_height = version_bits_params.min_activation_height; - } + ApplyDeploymentOptions(opts.dep_opts); genesis = CreateGenesisBlock(1296688602, 2, 0x207fffff, 1, 50 * COIN); consensus.hashGenesisBlock = genesis.GetHash(); @@ -660,19 +673,19 @@ std::unique_ptr CChainParams::RegTest(const RegTestOptions& return std::make_unique(options); } -std::unique_ptr CChainParams::Main() +std::unique_ptr CChainParams::Main(const MainNetOptions& options) { - return std::make_unique(); + return std::make_unique(options); } -std::unique_ptr CChainParams::TestNet() +std::unique_ptr CChainParams::TestNet(const TestNetOptions& options) { - return std::make_unique(); + return std::make_unique(options); } -std::unique_ptr CChainParams::TestNet4() +std::unique_ptr CChainParams::TestNet4(const TestNetOptions& options) { - return std::make_unique(); + return std::make_unique(options); } std::vector CChainParams::GetAvailableSnapshotHeights() const @@ -691,8 +704,8 @@ std::optional GetNetworkForMagic(const MessageStartChars& message) const auto mainnet_msg = CChainParams::Main()->MessageStart(); const auto testnet_msg = CChainParams::TestNet()->MessageStart(); const auto testnet4_msg = CChainParams::TestNet4()->MessageStart(); - const auto regtest_msg = CChainParams::RegTest({})->MessageStart(); - const auto signet_msg = CChainParams::SigNet({})->MessageStart(); + const auto regtest_msg = CChainParams::RegTest()->MessageStart(); + const auto signet_msg = CChainParams::SigNet()->MessageStart(); if (std::ranges::equal(message, mainnet_msg)) { return ChainType::MAIN; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index f7209bee1b06..2c85b8e80efa 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -127,14 +127,6 @@ class CChainParams const ChainTxData& TxData() const { return chainTxData; } - /** - * SigNetOptions holds configurations for creating a signet CChainParams. - */ - struct SigNetOptions { - std::optional> challenge{}; - std::optional> seeds{}; - }; - /** * VersionBitsParameters holds activation parameters */ @@ -144,21 +136,47 @@ class CChainParams int min_activation_height; }; + struct DeploymentOptions { + std::unordered_map version_bits_parameters{}; + std::unordered_map activation_heights{}; + }; + + /** + * SigNetOptions holds configurations for creating a signet CChainParams. + */ + struct SigNetOptions { + DeploymentOptions dep_opts{}; + std::optional> challenge{}; + std::optional> seeds{}; + }; + /** * RegTestOptions holds configurations for creating a regtest CChainParams. */ struct RegTestOptions { - std::unordered_map version_bits_parameters{}; - std::unordered_map activation_heights{}; + DeploymentOptions dep_opts{}; bool fastprune{false}; bool enforce_bip94{false}; }; + struct MainNetOptions { + DeploymentOptions dep_opts{}; + }; + + struct TestNetOptions { + DeploymentOptions dep_opts{}; + }; + static std::unique_ptr RegTest(const RegTestOptions& options); + static std::unique_ptr RegTest() { const RegTestOptions opts{}; return RegTest(opts); } static std::unique_ptr SigNet(const SigNetOptions& options); - static std::unique_ptr Main(); - static std::unique_ptr TestNet(); - static std::unique_ptr TestNet4(); + static std::unique_ptr SigNet() { const SigNetOptions opts{}; return SigNet(opts); } + static std::unique_ptr Main(const MainNetOptions& options); + static std::unique_ptr Main() { const MainNetOptions opts{}; return Main(opts); } + static std::unique_ptr TestNet(const TestNetOptions& options); + static std::unique_ptr TestNet() { const TestNetOptions opts{}; return TestNet(opts); } + static std::unique_ptr TestNet4(const TestNetOptions& options); + static std::unique_ptr TestNet4() { const TestNetOptions opts{}; return TestNet4(opts); } protected: CChainParams() = default; @@ -180,6 +198,8 @@ class CChainParams std::vector m_assumeutxo_data; ChainTxData chainTxData; HeadersSyncParams m_headers_sync_params; + + void ApplyDeploymentOptions(const DeploymentOptions& opts); }; std::optional GetNetworkForMagic(const MessageStartChars& pchMessageStart); diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp index b68ef58a156c..a161f5b40bf1 100644 --- a/src/test/fuzz/versionbits.cpp +++ b/src/test/fuzz/versionbits.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -31,7 +32,7 @@ class TestConditionChecker : public VersionBitsConditionChecker { assert(dep.period > 0); assert(dep.threshold <= dep.period); - assert(0 <= dep.bit && dep.bit < 32 && dep.bit < VERSIONBITS_NUM_BITS); + assert(0 <= dep.bit && dep.bit < 32 && dep.bit < VERSIONBITS_MAX_NUM_BITS); assert(0 <= dep.min_activation_height); } @@ -126,7 +127,7 @@ FUZZ_TARGET(versionbits, .init = initialize) assert(0 < dep.threshold && dep.threshold <= dep.period); // must be able to both pass and fail threshold! // select deployment parameters: bit, start time, timeout - dep.bit = fuzzed_data_provider.ConsumeIntegralInRange(0, VERSIONBITS_NUM_BITS - 1); + dep.bit = fuzzed_data_provider.ConsumeIntegralInRange(0, VERSIONBITS_MAX_NUM_BITS - 1); if (always_active_test) { dep.nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; diff --git a/src/test/util/versionbits.h b/src/test/util/versionbits.h new file mode 100644 index 000000000000..478b7882fab8 --- /dev/null +++ b/src/test/util/versionbits.h @@ -0,0 +1,13 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_TEST_UTIL_VERSIONBITS_H +#define BITCOIN_TEST_UTIL_VERSIONBITS_H + +#include + +/** Total possible bits available for versionbits per original BIP 9 specification */ +static constexpr int VERSIONBITS_MAX_NUM_BITS{29}; + +#endif // BITCOIN_TEST_UTIL_VERSIONBITS_H diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp index ad7920531175..77384c5f2f15 100644 --- a/src/test/versionbits_tests.cpp +++ b/src/test/versionbits_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -454,9 +455,19 @@ BOOST_FIXTURE_TEST_CASE(versionbits_computeblockversion, BlockVersionTest) // not take precedence over STARTED/LOCKED_IN. So all softforks on // the same bit might overlap, even when non-overlapping start-end // times are picked. - const uint32_t dep_mask{uint32_t{1} << chainParams->GetConsensus().vDeployments[dep].bit}; + const auto& dep_info = chainParams->GetConsensus().vDeployments[dep]; + const uint32_t dep_mask{uint32_t{1} << dep_info.bit}; BOOST_CHECK(!(chain_all_vbits & dep_mask)); chain_all_vbits |= dep_mask; + BOOST_CHECK(0 <= dep_info.bit && dep_info.bit < VERSIONBITS_MAX_NUM_BITS); + if (chain_type != ChainType::REGTEST) { + if (dep == Consensus::DEPLOYMENT_TESTDUMMY) { + BOOST_CHECK_EQUAL(dep_info.nStartTime, Consensus::BIP9Deployment::NEVER_ACTIVE); + BOOST_CHECK_EQUAL(dep_info.nTimeout, Consensus::BIP9Deployment::NO_TIMEOUT); + } else { + BOOST_CHECK(dep_info.bit < VERSIONBITS_NUM_BITS); + } + } check_computeblockversion(vbcache, chainParams->GetConsensus(), dep); } } diff --git a/src/versionbits.h b/src/versionbits.h index 59b0cbeee716..f88ead0dce60 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -21,8 +21,8 @@ static const int32_t VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4; static const int32_t VERSIONBITS_TOP_BITS = 0x20000000UL; /** What bitmask determines whether versionbits is in use */ static const int32_t VERSIONBITS_TOP_MASK = 0xE0000000UL; -/** Total bits available for versionbits */ -static const int32_t VERSIONBITS_NUM_BITS = 29; +/** Total bits available for versionbits (BIP 323) */ +static const int32_t VERSIONBITS_NUM_BITS = 5; /** Opaque type for BIP9 state. See versionbits_impl.h for details. */ enum class ThresholdState : uint8_t; @@ -72,7 +72,8 @@ struct BIP9GBTStatus { }; /** BIP 9 allows multiple softforks to be deployed in parallel. We cache - * per-period state for every one of them. */ + * per-period state for every one we implement and warning state for each + * BIP 323 allowed bit. */ class VersionBitsCache { private: diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index be1be83c4a9f..8d69957c302f 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -54,25 +54,47 @@ struct { * set that can pay for the spending target and does not exceed the spending target by more than the * cost of creating and spending a change output. The algorithm uses a depth-first search on a binary * tree. In the binary tree, each node corresponds to the inclusion or the omission of a UTXO. UTXOs - * are sorted by their effective values and the tree is explored deterministically per the inclusion - * branch first. At each node, the algorithm checks whether the selection is within the target range. + * are sorted by their effective values, tie-broken by their waste score, and the tree is explored deterministically per the inclusion + * branch first. For each new input set candidate, the algorithm checks whether the selection is within the target range. * While the selection has not reached the target range, more UTXOs are included. When a selection's - * value exceeds the target range, the complete subtree deriving from this selection can be omitted. + * value exceeds the target range, the complete subtree deriving from this selection prefix can be omitted. * At that point, the last included UTXO is deselected and the corresponding omission branch explored - * instead. The search ends after the complete tree has been searched or after a limited number of tries. + * instead starting by adding the subsequent UTXO. The search ends after the complete tree has been searched or after a limited number of tries. * - * The search continues to search for better solutions after one solution has been found. The best - * solution is chosen by minimizing the waste metric. The waste metric is defined as the cost to + * The algorithm continues to search for better solutions after one solution has been found. The best + * solution is chosen by minimal waste score. The waste metric is defined as the cost to * spend the current inputs at the given fee rate minus the long term expected cost to spend the - * inputs, plus the amount by which the selection exceeds the spending target: + * inputs, plus the amount by which the selection exceeds the spending target (the "excess"): * - * waste = selectionTotal - target + inputs × (currentFeeRate - longTermFeeRate) + * excess = selected_amount - target + * waste = inputs × (currentFeeRate - longTermFeeRate) + excess * - * The algorithm uses two additional optimizations. A lookahead keeps track of the total value of - * the unexplored UTXOs. A subtree is not explored if the lookahead indicates that the target range - * cannot be reached. Further, it is unnecessary to test equivalent combinations. This allows us - * to skip testing the inclusion of UTXOs that match the effective value and waste of an omitted - * predecessor. + * Note that this means that at fee rates higher than longTermFeeRate additional inputs increase the + * waste score, while at fee rates lower than longTermFeeRate additional inputs decrease the waste + * score. + * + * The algorithm uses the following optimizations: + * 1. Lookahead: The lookahead stores the total remaining effective value of the undecided UTXOs for + * every depth of the search tree. Whenever the currently selected amount plus the potential + * amount from the lookahead falls short of the target, we can immediately stop searching the + * subtree as no more input set candidates can be found in it. + * 2. Skip clones: When two UTXOs match in weight and effective value ("are clones"), naive + * exploration would cause redundant work: e.g., given the UTXOs A, A', and B, where A and A' are + * clones, naive exploration would combine (read underscore as omission): + * [{}, {A}, {A, A'}, {A, A', B}, {A, _, B}, {_, A'}, {_, A', B}, {_, _, B}]. + * In this case the input set candidates {A} and {A'} as well as {A, B} and {A', B} are + * equivalent. It is sufficient to explore combinations that select either both UTXOs or the + * first UTXO. Whenever the first UTXO is omitted, we can also skip the clone as we have already + * explored a set of equivalent combination as the one we could generate with the second clone. + * Concretely, we skip a UTXO when its predecessor is omitted and the UTXO matches the + * effective value and the waste of the predecessor. + * 3. Skip similar UTXOs that are more wasteful: This search algorithm operates on the list of UTXOs + * sorted by effective value, tie-broken to prefer lower waste. This means that among two + * subsequent UTXOs with the same effective value, the second UTXO’s waste score will either be + * equal _or higher_ than the first UTXO’s. This allows us to apply the clone skipping idea more + * broadly: any combination with the second UTXO is equivalent _or worse_ than what we already + * combined with the first UTXO. We skip a UTXO if its predecessor is omitted and the predecessor + * matches in effective value. * * The Branch and Bound algorithm is described in detail in Murch's Master Thesis: * https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf @@ -93,113 +115,165 @@ static const size_t TOTAL_TRIES = 100000; util::Result SelectCoinsBnB(std::vector& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, int max_selection_weight) { - SelectionResult result(selection_target, SelectionAlgorithm::BNB); - CAmount curr_value = 0; - std::vector curr_selection; // selected utxo indexes - int curr_selection_weight = 0; // sum of selected utxo weight - - // Calculate curr_available_value - CAmount curr_available_value = 0; - for (const OutputGroup& utxo : utxo_pool) { - // Assert that this utxo is not negative. It should never be negative, - // effective value calculation should have removed it - assert(utxo.GetSelectionAmount() > 0); - curr_available_value += utxo.GetSelectionAmount(); + std::sort(utxo_pool.begin(), utxo_pool.end(), descending); + // The sum of UTXO amounts after this UTXO index, e.g. lookahead[5] = Σ(UTXO[6+].amount) + std::vector lookahead(utxo_pool.size()); + + // Calculate lookahead values, and check that there are sufficient funds + CAmount total_available = 0; + for (int index = static_cast(utxo_pool.size()) - 1; index >= 0; --index) { + lookahead[index] = total_available; + // UTXOs with non-positive effective value must have been filtered + Assume(utxo_pool[index].GetSelectionAmount() > 0); + total_available += utxo_pool[index].GetSelectionAmount(); } - if (curr_available_value < selection_target) { + + if (total_available < selection_target) { + // Insufficient funds return util::Error(); } - // Sort the utxo_pool - std::sort(utxo_pool.begin(), utxo_pool.end(), descending); - CAmount curr_waste = 0; + // The current selection and the best input set found so far, stored as the utxo_pool indices of the UTXOs forming them + std::vector curr_selection; std::vector best_selection; + + // The currently selected effective amount + CAmount curr_amount = 0; + + // The waste score of the current selection, and the best waste score so far + CAmount curr_selection_waste = 0; CAmount best_waste = MAX_MONEY; - bool is_feerate_high = utxo_pool.at(0).fee > utxo_pool.at(0).long_term_fee; + // The weight of the currently selected input set + int curr_weight = 0; + + // Whether the input sets generated during this search have exceeded the maximum transaction weight at any point bool max_tx_weight_exceeded = false; - // Depth First search loop for choosing the UTXOs - for (size_t curr_try = 0, utxo_pool_index = 0; curr_try < TOTAL_TRIES; ++curr_try, ++utxo_pool_index) { - // Conditions for starting a backtrack - bool backtrack = false; - if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value. - curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch - (curr_waste > best_waste && is_feerate_high)) { // Don't select things which we know will be more wasteful if the waste is increasing - backtrack = true; - } else if (curr_selection_weight > max_selection_weight) { // Selected UTXOs weight exceeds the maximum weight allowed, cannot find more solutions by adding more inputs - max_tx_weight_exceeded = true; // at least one selection attempt exceeded the max weight - backtrack = true; - } else if (curr_value >= selection_target) { // Selected value is within range - curr_waste += (curr_value - selection_target); // This is the excess value which is added to the waste for the below comparison - // Adding another UTXO after this check could bring the waste down if the long term fee is higher than the current fee. - // However we are not going to explore that because this optimization for the waste is only done when we have hit our target - // value. Adding any more UTXOs will be just burning the UTXO; it will go entirely to fees. Thus we aren't going to - // explore any more UTXOs to avoid burning money like that. + // Index of the next UTXO to consider in utxo_pool + size_t next_utxo = 0; + + auto deselect_last = [&]() { + OutputGroup& utxo = utxo_pool[curr_selection.back()]; + curr_amount -= utxo.GetSelectionAmount(); + curr_weight -= utxo.m_weight; + curr_selection_waste -= utxo.fee - utxo.long_term_fee; + curr_selection.pop_back(); + }; + + size_t curr_try = 0; + SelectionResult result(selection_target, SelectionAlgorithm::BNB); + bool is_done = false; + // We don’t have access to the feerate here, but fee to long_term_fee is as feerate to LTFRE + bool is_feerate_high = utxo_pool.at(0).fee > utxo_pool.at(0).long_term_fee; + while (!is_done) { + bool should_shift{false}, should_cut{false}; + // Select `next_utxo` + OutputGroup& utxo = utxo_pool[next_utxo]; + curr_amount += utxo.GetSelectionAmount(); + curr_weight += utxo.m_weight; + curr_selection_waste += utxo.fee - utxo.long_term_fee; + curr_selection.push_back(next_utxo); + ++next_utxo; + ++curr_try; + + // EVALUATE current selection: check for solutions and see whether we can CUT or SHIFT before EXPLORING further + if (curr_amount + lookahead[curr_selection.back()] < selection_target) { + // Insufficient funds with lookahead: CUT + should_cut = true; + } else if (curr_weight > max_selection_weight) { + // max_weight exceeded: SHIFT + max_tx_weight_exceeded = true; + should_shift = true; + } else if (curr_amount > selection_target + cost_of_change) { + // Overshot target range: SHIFT + should_shift = true; + } else if (is_feerate_high && curr_selection_waste > best_waste) { + // At high feerates adding more inputs will increase the waste score. If the current waste is already worse + // than the best selection’s while we have insufficient funds, it is impossible for this partial selection + // to beat the best selection by adding more inputs: SHIFT + // At low feerates, additional inputs lower the waste score, and using this would cause us to skip exploring + // combinations with more inputs of lower amounts. + should_shift = true; + } else if (curr_amount >= selection_target) { + // Selection is within target window: potential solution + // Adding more UTXOs only increases fees and cannot be better: SHIFT + should_shift = true; + // The amount exceeding the selection_target (the "excess"), would be dropped to the fees: it is waste. + CAmount curr_excess = curr_amount - selection_target; + CAmount curr_waste = curr_selection_waste + curr_excess; if (curr_waste <= best_waste) { + // New best solution best_selection = curr_selection; best_waste = curr_waste; } - curr_waste -= (curr_value - selection_target); // Remove the excess value as we will be selecting different coins now - backtrack = true; } - if (backtrack) { // Backtracking, moving backwards - if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched - break; - } + if (curr_try >= TOTAL_TRIES) { + // Solution is not guaranteed to be optimal if `curr_try` hit TOTAL_TRIES + result.SetAlgoCompleted(false); + break; + } - // Add omitted UTXOs back to lookahead before traversing the omission branch of last included UTXO. - for (--utxo_pool_index; utxo_pool_index > curr_selection.back(); --utxo_pool_index) { - curr_available_value += utxo_pool.at(utxo_pool_index).GetSelectionAmount(); - } + if (next_utxo == utxo_pool.size()) { + // Last added UTXO was end of UTXO pool, nothing left to add on inclusion or omission branch: CUT + should_cut = true; + } - // Output was included on previous iterations, try excluding now. - assert(utxo_pool_index == curr_selection.back()); - OutputGroup& utxo = utxo_pool.at(utxo_pool_index); - curr_value -= utxo.GetSelectionAmount(); - curr_waste -= utxo.fee - utxo.long_term_fee; - curr_selection_weight -= utxo.m_weight; - curr_selection.pop_back(); - } else { // Moving forwards, continuing down this branch - OutputGroup& utxo = utxo_pool.at(utxo_pool_index); - - // Remove this utxo from the curr_available_value utxo amount - curr_available_value -= utxo.GetSelectionAmount(); - - if (curr_selection.empty() || - // The previous index is included and therefore not relevant for exclusion shortcut - (utxo_pool_index - 1) == curr_selection.back() || - // Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. - // Since the ratio of fee to long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same. - utxo.GetSelectionAmount() != utxo_pool.at(utxo_pool_index - 1).GetSelectionAmount() || - utxo.fee != utxo_pool.at(utxo_pool_index - 1).fee) - { - // Inclusion branch first (Largest First Exploration) - curr_selection.push_back(utxo_pool_index); - curr_value += utxo.GetSelectionAmount(); - curr_waste += utxo.fee - utxo.long_term_fee; - curr_selection_weight += utxo.m_weight; + if (should_cut) { + // Neither adding to the current selection nor exploring the omission branch of the last selected UTXO can + // find any solutions. Redirect to exploring the Omission branch of the penultimate selected UTXO (i.e. + // set `next_utxo` to one after the penultimate selected, then deselect the last two selected UTXOs) + deselect_last(); + should_shift = true; + } + + while (should_shift) { + if (curr_selection.empty()) { + // Exhausted search space before running into attempt limit + is_done = true; + result.SetAlgoCompleted(true); + break; + } + // Set `next_utxo` to one after last selected, then deselect last selected UTXO + next_utxo = curr_selection.back() + 1; + deselect_last(); + should_shift = false; + + // After SHIFTing to an omission branch, the `next_utxo` might have the same effective value as the + // UTXO we just omitted. Since lower waste is our tiebreaker on UTXOs with equal effective value for sorting, if it + // ties on the effective value, it _must_ have the same waste (i.e. be a "clone" of the prior UTXO) or a + // higher waste. If so, selecting `next_utxo` would produce an equivalent or worse + // selection as one we previously evaluated. In that case, increment `next_utxo` until we find a UTXO with a + // differing amount. + Assume(next_utxo < utxo_pool.size()); + while (utxo_pool[next_utxo - 1].GetSelectionAmount() == utxo_pool[next_utxo].GetSelectionAmount()) { + if (next_utxo >= utxo_pool.size() - 1) { + // Reached end of UTXO pool skipping clones: SHIFT instead + should_shift = true; + break; + } + // Skip clone: previous UTXO is equivalent and unselected + ++next_utxo; } } } - // Check for solution + result.SetSelectionsEvaluated(curr_try); + if (best_selection.empty()) { return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error(); } - // Set output set for (const size_t& i : best_selection) { result.AddInput(utxo_pool.at(i)); } - result.RecalculateWaste(cost_of_change, cost_of_change, CAmount{0}); - assert(best_waste == result.GetWaste()); return result; } + /* * TL;DR: Coin Grinder is a DFS-based algorithm that deterministically searches for the minimum-weight input set to fund * the transaction. The algorithm is similar to the Branch and Bound algorithm, but will produce a transaction _with_ a diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index 68969b6a0772..b4b865d6506a 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -112,7 +112,7 @@ static std::string InputAmountsToString(const SelectionResult& selection) return "[" + util::Join(selection.GetInputSet(), " ", [](const auto& input){ return util::ToString(input->txout.nValue);}) + "]"; } -static void TestBnBSuccess(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const std::vector& expected_input_amounts, const CoinSelectionParams& cs_params = default_cs_params, const int custom_spending_vsize = P2WPKH_INPUT_VSIZE, const int max_selection_weight = MAX_STANDARD_TX_WEIGHT) +static void TestBnBSuccess(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const std::vector& expected_input_amounts, size_t expected_attempts, const CoinSelectionParams& cs_params = default_cs_params, const int custom_spending_vsize = P2WPKH_INPUT_VSIZE, const int max_selection_weight = MAX_STANDARD_TX_WEIGHT) { SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); CAmount expected_amount = 0; @@ -127,6 +127,7 @@ static void TestBnBSuccess(std::string test_title, std::vector& utx BOOST_CHECK_MESSAGE(HaveEquivalentValues(expected_result, *result), strprintf("Result mismatch in BnB-Success: %s. Expected %s, but got %s", test_title, InputAmountsToString(expected_result), InputAmountsToString(*result))); BOOST_CHECK_MESSAGE(result->GetSelectedValue() == expected_amount, strprintf("Selected amount mismatch in BnB-Success: %s. Expected %d, but got %d", test_title, expected_amount, result->GetSelectedValue())); BOOST_CHECK_MESSAGE(result->GetWeight() <= max_selection_weight, strprintf("Selected weight is higher than permitted in BnB-Success: %s. Expected %d, but got %d", test_title, max_selection_weight, result->GetWeight())); + BOOST_CHECK_MESSAGE(result->GetSelectionsEvaluated() == expected_attempts, strprintf("Unexpected number of attempts in BnB-Success: %s. Expected %i attempts, but got %i", test_title, expected_attempts, result->GetSelectionsEvaluated())); } static void TestBnBFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) @@ -149,19 +150,19 @@ BOOST_AUTO_TEST_CASE(bnb_test) AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}, cs_params); // Simple success cases - TestBnBSuccess("Select smallest UTXO", utxo_pool, /*selection_target=*/1 * CENT, /*expected_input_amounts=*/{1 * CENT}, cs_params); - TestBnBSuccess("Select middle UTXO", utxo_pool, /*selection_target=*/3 * CENT, /*expected_input_amounts=*/{3 * CENT}, cs_params); - TestBnBSuccess("Select biggest UTXO", utxo_pool, /*selection_target=*/5 * CENT, /*expected_input_amounts=*/{5 * CENT}, cs_params); - TestBnBSuccess("Select two UTXOs", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params); - TestBnBSuccess("Select all UTXOs", utxo_pool, /*selection_target=*/9 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT, 5 * CENT}, cs_params); + TestBnBSuccess("Select smallest UTXO", utxo_pool, /*selection_target=*/1 * CENT, /*expected_input_amounts=*/{1 * CENT}, /*expected_attempts=*/3, cs_params); + TestBnBSuccess("Select middle UTXO", utxo_pool, /*selection_target=*/3 * CENT, /*expected_input_amounts=*/{3 * CENT}, /*expected_attempts=*/3, cs_params); + TestBnBSuccess("Select biggest UTXO", utxo_pool, /*selection_target=*/5 * CENT, /*expected_input_amounts=*/{5 * CENT}, /*expected_attempts=*/2, cs_params); + TestBnBSuccess("Select two UTXOs", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, /*expected_attempts=*/4, cs_params); + TestBnBSuccess("Select all UTXOs", utxo_pool, /*selection_target=*/9 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT, 5 * CENT}, /*expected_attempts=*/5, cs_params); // BnB finds changeless solution while overshooting by up to cost_of_change - TestBnBSuccess("Select upper bound", utxo_pool, /*selection_target=*/4 * CENT - cs_params.m_cost_of_change, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params); + TestBnBSuccess("Select upper bound", utxo_pool, /*selection_target=*/4 * CENT - cs_params.m_cost_of_change, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, /*expected_attempts=*/4, cs_params); // BnB fails to find changeless solution when overshooting by cost_of_change + 1 sat TestBnBFail("Overshoot upper bound", utxo_pool, /*selection_target=*/4 * CENT - cs_params.m_cost_of_change - 1, cs_params); - TestBnBSuccess("Select max weight", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params, /*custom_spending_vsize=*/P2WPKH_INPUT_VSIZE, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE); + TestBnBSuccess("Select max weight", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, /*expected_attempts=*/4, cs_params, /*custom_spending_vsize=*/P2WPKH_INPUT_VSIZE, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE); TestBnBFail("Exceed max weight", utxo_pool, /*selection_target=*/4 * CENT, cs_params, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE - 1, /*expect_max_weight_exceeded=*/true); @@ -174,7 +175,7 @@ BOOST_AUTO_TEST_CASE(bnb_test) std::vector clone_pool; AddCoins(clone_pool, {2 * CENT, 7 * CENT, 7 * CENT}, cs_params); AddDuplicateCoins(clone_pool, /*count=*/50'000, /*amount=*/5 * CENT, cs_params); - TestBnBSuccess("Skip equivalent input sets", clone_pool, /*selection_target=*/16 * CENT, /*expected_input_amounts=*/{2 * CENT, 7 * CENT, 7 * CENT}, cs_params); + TestBnBSuccess("Skip equivalent input sets", clone_pool, /*selection_target=*/16 * CENT, /*expected_input_amounts=*/{2 * CENT, 7 * CENT, 7 * CENT}, /*expected_attempts=*/16, cs_params); /* Test BnB attempt limit (`TOTAL_TRIES`) * @@ -206,12 +207,16 @@ BOOST_AUTO_TEST_CASE(bnb_test) } } AddCoins(doppelganger_pool, doppelgangers, cs_params); - // 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, cs_params); + // Among 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, /*expected_attempts=*/51'765, cs_params); - // Starting with 18 unique UTXOs of similar effective value we will not find the solution due to exceeding the attempt limit + // Among up to 18 unique UTXOs of similar effective value we will find a solution composed of the eight smallest UTXOs AddCoins(doppelganger_pool, {1 * CENT + cs_params.m_cost_of_change + 17}, cs_params); - TestBnBFail("Exhaust looking for smallest 8 of 18 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT, cs_params); + TestBnBSuccess("Combine smallest 8 of 18 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT, /*expected_input_amounts=*/expected_inputs, /*expected_attempts=*/87'957, cs_params); + + // Starting with 19 unique UTXOs of similar effective value we will not find the solution due to exceeding the attempt limit + AddCoins(doppelganger_pool, {1 * CENT + cs_params.m_cost_of_change + 18}, cs_params); + TestBnBFail("Exhaust looking for smallest 8 of 19 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT, cs_params); } } @@ -220,21 +225,21 @@ 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 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}); + TestBnBSuccess("Select many inputs at low feerates", low_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{2 * CENT, 3 * CENT, 5 * CENT}, /*expected_attempts=*/6); const CoinSelectionParams high_feerate_params = init_cs_params(/*eff_feerate=*/25'000); std::vector 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); + TestBnBSuccess("Select one input at high feerates", high_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{10 * CENT}, /*expected_attempts=*/5, high_feerate_params); // Add heavy inputs {6, 7} to existing {2, 3, 5, 10} low_feerate_pool.push_back(MakeCoin(6 * CENT, true, default_cs_params, /*custom_spending_vsize=*/500)); low_feerate_pool.push_back(MakeCoin(7 * CENT, true, default_cs_params, /*custom_spending_vsize=*/500)); - TestBnBSuccess("Prefer two heavy inputs over two light inputs at low feerates", low_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{6 * CENT, 7 * CENT}, default_cs_params, /*custom_spending_vsize=*/500); + TestBnBSuccess("Prefer two heavy inputs over two light inputs at low feerates", low_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{6 * CENT, 7 * CENT}, /*expected_attempts=*/18, default_cs_params, /*custom_spending_vsize=*/500); high_feerate_pool.push_back(MakeCoin(6 * CENT, true, high_feerate_params, /*custom_spending_vsize=*/500)); high_feerate_pool.push_back(MakeCoin(7 * CENT, true, high_feerate_params, /*custom_spending_vsize=*/500)); - TestBnBSuccess("Prefer two light inputs over two heavy inputs at high feerates", high_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{3 * CENT, 10 * CENT}, high_feerate_params); + TestBnBSuccess("Prefer two light inputs over two heavy inputs at high feerates", high_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{3 * CENT, 10 * CENT}, /*expected_attempts=*/9, high_feerate_params); } static void TestSRDSuccess(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, const int max_selection_weight = MAX_STANDARD_TX_WEIGHT) diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index ee6a639a43db..17278a685179 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -171,6 +171,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) // Setup std::vector utxo_pool; SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); + size_t expected_attempts; //////////////////// // Behavior tests // @@ -208,6 +209,8 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) const auto result9 = SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change); BOOST_CHECK(result9); BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT); + expected_attempts = 1; + BOOST_CHECK_MESSAGE(result9->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result9->GetSelectionsEvaluated())); } { @@ -230,6 +233,8 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) LOCK(wallet->cs_wallet); const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); BOOST_CHECK(result10); + expected_attempts = 3; + BOOST_CHECK_MESSAGE(result10->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result10->GetSelectionsEvaluated())); } { std::unique_ptr wallet = NewWallet(m_node); @@ -259,6 +264,8 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint}); const auto result13 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); BOOST_CHECK(EquivalentResult(expected_result, *result13)); + expected_attempts = 2; + BOOST_CHECK_MESSAGE(result13->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result13->GetSelectionsEvaluated())); } { @@ -290,6 +297,8 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(5 * CENT, 2, expected_result); add_coin(3 * CENT, 2, expected_result); BOOST_CHECK(EquivalentResult(expected_result, *res)); + expected_attempts = 22; + BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); } } diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index e1e9ff4356ba..f630f7817231 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -18,8 +18,13 @@ VB_PERIOD = 144 # versionbits period length for regtest VB_THRESHOLD = 108 # versionbits activation threshold for regtest VB_TOP_BITS = 0x20000000 -VB_UNKNOWN_BIT = 27 # Choose a bit unassigned to any deployment + +# Choose a bit unassigned to any deployment, or start the +# node with the deployment matching this bit disabled. +VB_UNKNOWN_BIT = 3 VB_UNKNOWN_VERSION = VB_TOP_BITS | (1 << VB_UNKNOWN_BIT) +VB_IGNORED_BIT = 5 +VB_IGNORED_VERSION = VB_TOP_BITS | (1 << VB_IGNORED_BIT) WARN_UNKNOWN_RULES_ACTIVE = f"Unknown new rules activated (versionbit {VB_UNKNOWN_BIT})" VB_PATTERN = re.compile("Unknown new rules activated.*versionbit") @@ -76,11 +81,24 @@ def run_test(self): assert not VB_PATTERN.match(",".join(node.getmininginfo()["warnings"])) assert not VB_PATTERN.match(",".join(node.getnetworkinfo()["warnings"])) + self.log.info("Check that there is no warning if previous VB_BLOCKS have VB_PERIOD blocks with ignored versionbits version.") # Build one period of blocks with VB_THRESHOLD blocks signaling some unknown bit - self.send_blocks_with_version(peer, VB_THRESHOLD, VB_UNKNOWN_VERSION) + self.send_blocks_with_version(peer, VB_THRESHOLD, VB_IGNORED_VERSION) self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD, node_deterministic_address) + # Move the ignored deployment state to ACTIVE and make sure we're out of IBD. + self.generatetoaddress(node, VB_PERIOD, node_deterministic_address) + self.wait_until(lambda: not node.getblockchaininfo()['initialblockdownload']) + + # Check that we're not getting any versionbit-related warnings in get*info() + assert not VB_PATTERN.match(", ".join(node.getmininginfo()["warnings"])) + assert not VB_PATTERN.match(", ".join(node.getnetworkinfo()["warnings"])) + self.log.info("Check that there is a warning if previous VB_BLOCKS have >=VB_THRESHOLD blocks with unknown versionbits version.") + # Build one period of blocks with VB_THRESHOLD blocks signaling some unknown bit + self.send_blocks_with_version(peer, VB_THRESHOLD, VB_UNKNOWN_VERSION) + self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD, node_deterministic_address) + # Mine a period worth of expected blocks so the generic block-version warning # is cleared. This will move the versionbit state to ACTIVE. self.generatetoaddress(node, VB_PERIOD, node_deterministic_address)