diff --git a/configure.ac b/configure.ac index 419f2b537732..4dd273e57548 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ AC_PREREQ([2.69]) dnl Don't forget to push a corresponding tag when updating any of _CLIENT_VERSION_* numbers define(_CLIENT_VERSION_MAJOR, 23) define(_CLIENT_VERSION_MINOR, 1) -define(_CLIENT_VERSION_BUILD, 7) +define(_CLIENT_VERSION_BUILD, 8) define(_CLIENT_VERSION_IS_RELEASE, false) define(_COPYRIGHT_YEAR, 2026) define(_COPYRIGHT_HOLDERS,[The %s developers]) diff --git a/contrib/flatpak/org.dash.dash-core.metainfo.xml b/contrib/flatpak/org.dash.dash-core.metainfo.xml index 30227f6b4771..66e742659443 100644 --- a/contrib/flatpak/org.dash.dash-core.metainfo.xml +++ b/contrib/flatpak/org.dash.dash-core.metainfo.xml @@ -21,6 +21,7 @@ + diff --git a/doc/man/dash-cli.1 b/doc/man/dash-cli.1 index 2d5739f408f7..8bf92ce8161b 100644 --- a/doc/man/dash-cli.1 +++ b/doc/man/dash-cli.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASH-CLI "1" "June 2026" "dash-cli v23.1.7" "User Commands" +.TH DASH-CLI "1" "July 2026" "dash-cli v23.1.8" "User Commands" .SH NAME -dash-cli \- manual page for dash-cli v23.1.7 +dash-cli \- manual page for dash-cli v23.1.8 .SH SYNOPSIS .B dash-cli [\fI\,options\/\fR] \fI\, \/\fR[\fI\,params\/\fR] \fI\,Send command to Dash Core\/\fR @@ -15,7 +15,7 @@ dash-cli \- manual page for dash-cli v23.1.7 .B dash-cli [\fI\,options\/\fR] \fI\,help Get help for a command\/\fR .SH DESCRIPTION -Dash Core RPC client version v23.1.7 +Dash Core RPC client version v23.1.8 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-qt.1 b/doc/man/dash-qt.1 index 30cb48525d9c..00cba193fda8 100644 --- a/doc/man/dash-qt.1 +++ b/doc/man/dash-qt.1 @@ -1,12 +1,12 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASH-QT "1" "June 2026" "dash-qt v23.1.7" "User Commands" +.TH DASH-QT "1" "July 2026" "dash-qt v23.1.8" "User Commands" .SH NAME -dash-qt \- manual page for dash-qt v23.1.7 +dash-qt \- manual page for dash-qt v23.1.8 .SH SYNOPSIS .B dash-qt [\fI\,command-line options\/\fR] [\fI\,URI\/\fR] .SH DESCRIPTION -Dash Core version v23.1.7 +Dash Core version v23.1.8 .PP Optional URI is a Dash address in BIP21 URI format. .SH OPTIONS @@ -128,13 +128,13 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (0 = auto, <0 = leave that many -cores free, max: 15, default: 0) +Set the number of script verification threads (0 = auto, <0 = leave that +many cores free, max: 15, default: 0) .HP \fB\-parbls=\fR .IP -Set the number of BLS verification threads (0 = auto, <0 = leave that many -cores free, max: 33, default: 0) +Set the number of BLS verification threads (0 = auto, <0 = leave that +many cores free, max: 33, default: 0) .HP \fB\-persistmempool\fR .IP diff --git a/doc/man/dash-tx.1 b/doc/man/dash-tx.1 index fa7fc530064b..7b587eeb9f19 100644 --- a/doc/man/dash-tx.1 +++ b/doc/man/dash-tx.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASH-TX "1" "June 2026" "dash-tx v23.1.7" "User Commands" +.TH DASH-TX "1" "July 2026" "dash-tx v23.1.8" "User Commands" .SH NAME -dash-tx \- manual page for dash-tx v23.1.7 +dash-tx \- manual page for dash-tx v23.1.8 .SH SYNOPSIS .B dash-tx [\fI\,options\/\fR] \fI\, \/\fR[\fI\,commands\/\fR] \fI\,Update hex-encoded dash transaction\/\fR @@ -9,7 +9,7 @@ dash-tx \- manual page for dash-tx v23.1.7 .B dash-tx [\fI\,options\/\fR] \fI\,-create \/\fR[\fI\,commands\/\fR] \fI\,Create hex-encoded dash transaction\/\fR .SH DESCRIPTION -Dash Core dash\-tx utility version v23.1.7 +Dash Core dash\-tx utility version v23.1.8 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-util.1 b/doc/man/dash-util.1 index f1a1e4ccf654..3a47b0b8037a 100644 --- a/doc/man/dash-util.1 +++ b/doc/man/dash-util.1 @@ -1,12 +1,12 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASH-UTIL "1" "June 2026" "dash-util v23.1.7" "User Commands" +.TH DASH-UTIL "1" "July 2026" "dash-util v23.1.8" "User Commands" .SH NAME -dash-util \- manual page for dash-util v23.1.7 +dash-util \- manual page for dash-util v23.1.8 .SH SYNOPSIS .B dash-util [\fI\,options\/\fR] [\fI\,commands\/\fR] \fI\,Do stuff\/\fR .SH DESCRIPTION -Dash Core dash\-util utility version v23.1.7 +Dash Core dash\-util utility version v23.1.8 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-wallet.1 b/doc/man/dash-wallet.1 index b942f301f180..ffeaccdb710f 100644 --- a/doc/man/dash-wallet.1 +++ b/doc/man/dash-wallet.1 @@ -1,9 +1,9 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASH-WALLET "1" "June 2026" "dash-wallet v23.1.7" "User Commands" +.TH DASH-WALLET "1" "July 2026" "dash-wallet v23.1.8" "User Commands" .SH NAME -dash-wallet \- manual page for dash-wallet v23.1.7 +dash-wallet \- manual page for dash-wallet v23.1.8 .SH DESCRIPTION -Dash Core dash\-wallet version v23.1.7 +Dash Core dash\-wallet version v23.1.8 .PP dash\-wallet is an offline tool for creating and interacting with Dash Core wallet files. By default dash\-wallet will act on wallets in the default mainnet wallet directory in the datadir. diff --git a/doc/man/dashd.1 b/doc/man/dashd.1 index 8312159ffd62..3f2e77656af2 100644 --- a/doc/man/dashd.1 +++ b/doc/man/dashd.1 @@ -1,12 +1,12 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DASHD "1" "June 2026" "dashd v23.1.7" "User Commands" +.TH DASHD "1" "July 2026" "dashd v23.1.8" "User Commands" .SH NAME -dashd \- manual page for dashd v23.1.7 +dashd \- manual page for dashd v23.1.8 .SH SYNOPSIS .B dashd [\fI\,options\/\fR] \fI\,Start Dash Core\/\fR .SH DESCRIPTION -Dash Core version v23.1.7 +Dash Core version v23.1.8 .SH OPTIONS .HP \-? @@ -126,13 +126,13 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (0 = auto, <0 = leave that many -cores free, max: 15, default: 0) +Set the number of script verification threads (0 = auto, <0 = leave that +many cores free, max: 15, default: 0) .HP \fB\-parbls=\fR .IP -Set the number of BLS verification threads (0 = auto, <0 = leave that many -cores free, max: 33, default: 0) +Set the number of BLS verification threads (0 = auto, <0 = leave that +many cores free, max: 33, default: 0) .HP \fB\-persistmempool\fR .IP diff --git a/doc/release-notes.md b/doc/release-notes.md index 5e4e5e7628b3..2a8d804abe00 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,8 +1,10 @@ -# Dash Core version v23.1.7 +# Dash Core version v23.1.8 -This is a new patch version release, bringing security hardening and build fixes -for newer compiler toolchains. -This release is **recommended** for all nodes, and especially for masternodes. +This is a new patch version release, fixing three remotely reachable crashes and +bringing further hardening of the peer-to-peer message handlers along with +networking, RPC and build fixes. +Upgrading is **strongly recommended** for all nodes, and required for +masternodes. Please report bugs using the issue tracker at GitHub: @@ -26,34 +28,98 @@ require a reindex. # Release Notes +## Critical fixes + +This release fixes three crashes that a remote party could trigger. None of +them affect consensus rules or put funds at risk, but each one can take a node +offline, so all operators should upgrade promptly. + +- Fixed a crash while removing provider transactions that a masternode's + operator-key change invalidates. Those transactions are collected before any + of them are removed, so when one was an in-mempool descendant of another it + was already erased along with its ancestor, and the stale entry was then + dereferenced. Such entries are now skipped. This is reachable whenever a block + carries a provider registrar update or revocation for a masternode that has + chained service updates pending in the mempool. +- Fixed a crash caused by an unvalidated LLMQ type in a `qsigshare` message. A + masternode that received a signature share naming an LLMQ type its chain does + not register would index a per-type quorum cache that is only populated for + known types, aborting the process. Unregistered types are now rejected before + the lookup, and the affected cache lookups no longer create missing entries. +- Fixed a crash caused by a quorum commitment naming a block with no parent, + such as the genesis block. The parentless block index reached a non-null + precondition and terminated the process instead of failing validation, which + no exception handler could contain. Commitments with a parentless quorum base + block are now rejected, and the LLMQ activation check treats a null + predecessor as "not enabled" rather than a contract violation. + ## Security -This release hardens several peer-to-peer message handlers against +This release continues the hardening of peer-to-peer message handlers against denial-of-service from remote peers. These issues do not affect consensus and do not put funds at risk, but they could be used to crash or degrade nodes - masternodes in particular - so upgrading is recommended. -- Networking: a peer whose receive buffer filled up could keep the socket-handler - thread spinning at 100% CPU for the duration of the backpressure. The thread now - falls back to its normal poll wait while such peers are paused. -- LLMQ / DKG: pushed DKG messages are now accepted only from verified masternodes, - are bounded in size, and are structurally validated before being retained; - malformed signatures can no longer trigger an assertion failure during batch - signature verification. -- BLS: verifying a DKG contribution share whose verification vector was never - received no longer dereferences a null pointer. -- InstantSend: locks with an oversized input set are now rejected before any - expensive processing, and the queues holding not-yet-verified and - awaiting-transaction locks are bounded to prevent unbounded memory growth. -- Governance: vote-sync requests carrying a bloom filter outside the permitted size - are rejected, preventing a CPU-amplification stall of P2P message processing. - -## Build - -- Fixed GCC 16 build failures in warning-enabled builds by tightening header - includes and initializing LevelDB compaction output size. - -# v23.1.7 Change log +- LLMQ / signing: the queues of not-yet-verified recovered signatures and + signature shares are now bounded, and the vectors carried by the QSIGSHARE, + QSIGSESANN, QSIGSHARESINV, QGETSIGSHARES and QBSIGSHARES messages are bounded + before any allocation or decoding takes place. The number of signing share + sessions a single peer may announce is also capped, so a peer can no longer + grow that per-peer state without limit (dash#7351). +- LLMQ / DKG: the number of encrypted contribution blobs in a DKG contribution + is now checked against the quorum's lower bound as well as its upper bound. +- LLMQ / quorum data: the verification vector and encrypted contribution + vectors in QDATA responses are validated against their expected sizes before + any BLS decoding is performed. +- Transaction relay: an oversized `notfound` message is now penalised rather + than silently ignored (dash#7348). +- ChainLocks: the cache of seen ChainLock signatures is now bounded. +- Governance: per-object vote sync requests are now throttled per peer, and + governance object and vote responses are only accepted from a peer if that + peer announced them or they were requested from it, using the net-layer + per-peer request tracker. Governance vote signatures are bounded when read + from the network and must use one of the two legitimate encodings. +- CoinJoin: the vectors carried by CoinJoin mixing messages are bounded before + allocation, and a non-participant can no longer abort another session's + signing phase. An invalid `dstx` message now carries a misbehaviour score + instead of being dropped for free (dash#7347). +- Bloom filters: filterload and filteradd payloads are bounded before + allocation. +- Sporks: spork signatures are bounded during deserialization, and malformed + spork messages now attribute misbehaviour to the sending peer. +- Compact block relay: batched hardening backported from upstream Bitcoin Core + (dash#7398), including detection of mutated blocks as a defence-in-depth + measure. + +## RPC + +- `protx listdiff` no longer reports an always-zero `platformP2PPort` / + `platformHTTPPort` for masternodes registered with extended addresses; the + live Platform ports are reported instead. + +## GUI + +- The PoSe score column is no longer hidden together with banned masternodes in + the masternode list. +- Fixed an abort when scaling widgets whose font was set in pixels rather than + points (for example by a stylesheet's `font-size: Npx`); such fonts are now + converted to a point size instead of being assumed to have one (dash#7465). + +## Build and CI + +- Fixed a CMake compatibility error when building the freetype dependency with + newer CMake (dash#7372). +- Stabilized the `-par` / `-parbls` help text (and the generated man pages) so + they no longer embed the core count of the build machine. +- Updated GitHub Actions pins for the Node 24 runtime. +- Fixed the circular-dependencies lint script under Python 3.15. + +## Tests + +- Governance inventory cache coverage moved from a functional test to unit + tests, and governance vote test fixtures are now wire-valid. + +# v23.1.8 Change log See detailed [set of changes][set-of-changes]. @@ -61,8 +127,10 @@ See detailed [set of changes][set-of-changes]. Thanks to everyone who directly contributed to this release: -- knst +- Konstantin Akimov +- PastaClaw - PastaPastaPasta +- UdjinM6 As well as everyone that submitted issues, reviewed pull requests and helped debug the release candidates. @@ -71,6 +139,7 @@ debug the release candidates. These releases are considered obsolete. Old release notes can be found here: +- [v23.1.7](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.7.md) released Jun/30/2026 - [v23.1.5](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.5.md) released Jun/19/2026 - [v23.1.4](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.4.md) released Jun/18/2026 - [v23.1.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.3.md) released May/28/2026 @@ -89,4 +158,4 @@ These releases are considered obsolete. Old release notes can be found here: - [v21.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-21.0.0.md) released Jul/25/2024 - [v20.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.1.1.md) released April/3/2024 -[set-of-changes]: https://github.com/dashpay/dash/compare/v23.1.5...dashpay:v23.1.7 +[set-of-changes]: https://github.com/dashpay/dash/compare/v23.1.7...dashpay:v23.1.8 diff --git a/doc/release-notes/dash/release-notes-23.1.7.md b/doc/release-notes/dash/release-notes-23.1.7.md new file mode 100644 index 000000000000..5e4e5e7628b3 --- /dev/null +++ b/doc/release-notes/dash/release-notes-23.1.7.md @@ -0,0 +1,92 @@ +# Dash Core version v23.1.7 + +This is a new patch version release, bringing security hardening and build fixes +for newer compiler toolchains. +This release is **recommended** for all nodes, and especially for masternodes. + +Please report bugs using the issue tracker at GitHub: + + + +# Upgrading and downgrading + +## How to Upgrade + +If you are running an older version, shut it down. Wait until it has completely +shut down (which might take a few minutes for older versions), then run the +installer (on Windows) or just copy over /Applications/Dash-Qt (on Mac) or +dashd/dash-qt (on Linux). + +## Downgrade warning + +### Downgrade to a version < v23.0.0 + +Downgrading to a version older than v23.0.0 is not supported, and will +require a reindex. + +# Release Notes + +## Security + +This release hardens several peer-to-peer message handlers against +denial-of-service from remote peers. These issues do not affect consensus and do +not put funds at risk, but they could be used to crash or degrade nodes - +masternodes in particular - so upgrading is recommended. + +- Networking: a peer whose receive buffer filled up could keep the socket-handler + thread spinning at 100% CPU for the duration of the backpressure. The thread now + falls back to its normal poll wait while such peers are paused. +- LLMQ / DKG: pushed DKG messages are now accepted only from verified masternodes, + are bounded in size, and are structurally validated before being retained; + malformed signatures can no longer trigger an assertion failure during batch + signature verification. +- BLS: verifying a DKG contribution share whose verification vector was never + received no longer dereferences a null pointer. +- InstantSend: locks with an oversized input set are now rejected before any + expensive processing, and the queues holding not-yet-verified and + awaiting-transaction locks are bounded to prevent unbounded memory growth. +- Governance: vote-sync requests carrying a bloom filter outside the permitted size + are rejected, preventing a CPU-amplification stall of P2P message processing. + +## Build + +- Fixed GCC 16 build failures in warning-enabled builds by tightening header + includes and initializing LevelDB compaction output size. + +# v23.1.7 Change log + +See detailed [set of changes][set-of-changes]. + +# Credits + +Thanks to everyone who directly contributed to this release: + +- knst +- PastaPastaPasta + +As well as everyone that submitted issues, reviewed pull requests and helped +debug the release candidates. + +# Older releases + +These releases are considered obsolete. Old release notes can be found here: + +- [v23.1.5](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.5.md) released Jun/19/2026 +- [v23.1.4](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.4.md) released Jun/18/2026 +- [v23.1.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.3.md) released May/28/2026 +- [v23.1.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.2.md) released Mar/12/2026 +- [v23.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.1.0.md) released Feb/15/2026 +- [v23.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.0.2.md) released Dec/4/2025 +- [v23.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-23.0.0.md) released Nov/10/2025 +- [v22.1.3](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-22.1.3.md) released Jul/15/2025 +- [v22.1.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-22.1.2.md) released Apr/15/2025 +- [v22.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-22.1.1.md) released Feb/17/2025 +- [v22.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-22.1.0.md) released Feb/10/2025 +- [v22.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-22.0.0.md) released Dec/12/2024 +- [v21.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-21.1.1.md) released Oct/22/2024 +- [v21.1.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-21.1.0.md) released Aug/8/2024 +- [v21.0.2](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-21.0.2.md) released Aug/1/2024 +- [v21.0.0](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-21.0.0.md) released Jul/25/2024 +- [v20.1.1](https://github.com/dashpay/dash/blob/master/doc/release-notes/dash/release-notes-20.1.1.md) released April/3/2024 + +[set-of-changes]: https://github.com/dashpay/dash/compare/v23.1.5...dashpay:v23.1.7 diff --git a/src/Makefile.test.include b/src/Makefile.test.include index faaee5aa1913..5e9d47f8c4c7 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -140,6 +140,7 @@ BITCOIN_TESTS =\ test/llmq_chainlock_tests.cpp \ test/llmq_commitment_tests.cpp \ test/llmq_hash_tests.cpp \ + test/llmq_invalid_type_tests.cpp \ test/llmq_params_tests.cpp \ test/llmq_snapshot_tests.cpp \ test/llmq_utils_tests.cpp \ diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index baa6adfd5334..b044f7c13dad 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -586,14 +586,31 @@ uint256 CQuorumBlockProcessor::GetQuorumBlockHash(const Consensus::LLMQParams& l bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const { bool fExists; - if (LOCK(minableCommitmentsCs); mapHasMinedCommitmentCache[llmqType].get(quorumHash, fExists)) { - return fExists; + { + // Defence-in-depth: this map is only pre-seeded by InitQuorumsCache() with the LLMQ types + // from the chain's consensus params. operator[] with any other type would insert a + // default-constructed, zero-capacity cache and abort in its constructor, so treat an + // unregistered type as "no mined commitment" rather than indexing the map. + LOCK(minableCommitmentsCs); + auto it = mapHasMinedCommitmentCache.find(llmqType); + if (it == mapHasMinedCommitmentCache.end()) { + return false; + } + if (it->second.get(quorumHash, fExists)) { + return fExists; + } } fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash))); - LOCK(minableCommitmentsCs); - mapHasMinedCommitmentCache[llmqType].insert(quorumHash, fExists); + { + LOCK(minableCommitmentsCs); + // The key set is fixed at construction, so this can only miss if the type was unregistered, + // which the check above already returned on. + if (auto it = mapHasMinedCommitmentCache.find(llmqType); it != mapHasMinedCommitmentCache.end()) { + it->second.insert(quorumHash, fExists); + } + } return fExists; } diff --git a/src/llmq/commitment.cpp b/src/llmq/commitment.cpp index 596c9f8599c8..b6726112230e 100644 --- a/src/llmq/commitment.cpp +++ b/src/llmq/commitment.cpp @@ -35,7 +35,6 @@ CFinalCommitment::CFinalCommitment(const Consensus::LLMQParams& params, const ui bool CFinalCommitment::VerifySignatureAsync(const llmq::UtilParameters& util_params, CCheckQueueControl* queue_control) const { - auto members = utils::GetAllQuorumMembers(llmqType, util_params); const auto& llmq_params_opt = Params().GetLLMQ(llmqType); if (!llmq_params_opt.has_value()) { LogPrint(BCLog::LLMQ, "CFinalCommitment -- q[%s] invalid llmqType=%d\n", quorumHash.ToString(), @@ -44,6 +43,19 @@ bool CFinalCommitment::VerifySignatureAsync(const llmq::UtilParameters& util_par } const auto& llmq_params = llmq_params_opt.value(); + // Reachable directly from CQuorumBlockProcessor::ProcessBlock(), not only via Verify(), so + // repeat its parentless-base check here. + if (util_params.m_base_index->pprev == nullptr) { + LogPrint(BCLog::LLMQ, "CFinalCommitment -- q[%s] parentless quorum base block\n", quorumHash.ToString()); + return false; + } + + auto members = utils::GetAllQuorumMembers(llmqType, util_params); + if (llmq_params.is_single_member() && members.empty()) { + LogPrint(BCLog::LLMQ, "CFinalCommitment -- q[%s] no quorum members\n", quorumHash.ToString()); + return false; + } + uint256 commitmentHash = BuildCommitmentHash(llmq_params.type, quorumHash, validMembers, quorumPublicKey, quorumVvecHash); if (LogAcceptDebug(BCLog::LLMQ)) { @@ -107,6 +119,14 @@ bool CFinalCommitment::Verify(const llmq::UtilParameters& util_params, bool chec } const auto& llmq_params = llmq_params_opt.value(); + // A quorum base block must have a parent; genesis can never host a quorum. quorumHash is + // attacker-supplied (a mined commitment or an unsolicited qfcommit), and the members lookup + // below dereferences m_base_index->pprev, so reject it explicitly here. + if (util_params.m_base_index->pprev == nullptr) { + LogPrint(BCLog::LLMQ, "CFinalCommitment -- q[%s] parentless quorum base block\n", quorumHash.ToString()); + return false; + } + const uint16_t expected_nversion{ CFinalCommitment::GetVersion(IsQuorumRotationEnabled(llmq_params, util_params.m_base_index), DeploymentActiveAfter(util_params.m_base_index, util_params.m_chainman.GetConsensus(), diff --git a/src/llmq/net_dkg.cpp b/src/llmq/net_dkg.cpp index e533aadf8777..8659b3be6530 100644 --- a/src/llmq/net_dkg.cpp +++ b/src/llmq/net_dkg.cpp @@ -435,6 +435,12 @@ void NetDKG::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStre m_peer_manager->PeerMisbehaving(pfrom.GetId(), 10); return; } + // Genesis (or any parentless index) is never a valid quorum base. + if (pQuorumBaseBlockIndex->pprev == nullptr) { + LogPrintf("NetDKG -- parentless quorumHash %s\n", quorumHash.ToString()); + m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100); + return; + } if (!m_chainman.IsQuorumTypeEnabled(llmqType, pQuorumBaseBlockIndex->pprev)) { LogPrintf("NetDKG -- llmqType [%d] quorums aren't active\n", std23::to_underlying(llmqType)); m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100); diff --git a/src/llmq/net_signing.cpp b/src/llmq/net_signing.cpp index 0883358b14c8..bc02a8883f43 100644 --- a/src/llmq/net_signing.cpp +++ b/src/llmq/net_signing.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -76,6 +77,17 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData } for (const auto& sigShare : receivedSigShares) { + // The LLMQ type comes straight off the wire as an unvalidated uint8_t. Reject types + // this chain doesn't register before touching ProcessMessageSigShare(): the quorum + // caches are keyed by LLMQ type and only pre-seeded for Params().GetConsensus().llmqs, + // so an unknown type would default-construct a zero-capacity cache and abort the node. + // The sibling handlers (QSIGREC, QSIGSESANN, QFCOMMITMENT, QGETDATA) gate the same way. + if (!Params().GetLLMQ(sigShare.getLlmqType()).has_value()) { + LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- invalid llmqType[%d] from peer=%d\n", __func__, + std23::to_underlying(sigShare.getLlmqType()), pfrom.GetId()); + BanNode(pfrom.GetId()); + return; + } if (!m_shares_manager->ProcessMessageSigShare(pfrom.GetId(), sigShare)) { BanNode(pfrom.GetId()); } diff --git a/src/llmq/quorumsman.cpp b/src/llmq/quorumsman.cpp index 7b64829e71cc..d340631f6636 100644 --- a/src/llmq/quorumsman.cpp +++ b/src/llmq/quorumsman.cpp @@ -355,8 +355,16 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_nul } CQuorumPtr pQuorum; - if (LOCK(m_cs_maps); mapQuorumsCache[llmqType].get(quorumHash, pQuorum)) { - return pQuorum; + { + // Defence-in-depth: mapQuorumsCache only holds the LLMQ types InitQuorumsCache() seeded + // from the chain's consensus params. operator[] on any other type would insert a + // default-constructed, zero-capacity cache and abort in its constructor, so look up + // without inserting and fall through for unknown types. + LOCK(m_cs_maps); + auto it = mapQuorumsCache.find(llmqType); + if (it != mapQuorumsCache.end() && it->second.get(quorumHash, pQuorum)) { + return pQuorum; + } } return BuildQuorumFromCommitment(llmqType, pQuorumBaseBlockIndex, populate_cache); @@ -398,8 +406,11 @@ CQuorumManager::DataResponseValidation CQuorumManager::ValidateDataResponse( CQuorumPtr CQuorumManager::GetCachedMutableQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) const { CQuorumPtr pQuorum; + // See GetQuorum(): never operator[] this map with a wire-supplied LLMQ type. LOCK(m_cs_maps); - mapQuorumsCache[llmqType].get(quorumHash, pQuorum); + if (auto it = mapQuorumsCache.find(llmqType); it != mapQuorumsCache.end()) { + it->second.get(quorumHash, pQuorum); + } return pQuorum; } diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 106af38c2c68..7155a1a98dce 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -637,7 +637,11 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame static RecursiveMutex cs_indexed_members; static std::map, QuorumMembers, StaticSaltedHasher>> mapIndexedQuorumMembers GUARDED_BY(cs_indexed_members); - if (!util_params.m_chainman.IsQuorumTypeEnabled(llmqType, util_params.m_base_index->pprev)) { + // A parentless base index (genesis) can never host a quorum. IsQuorumTypeEnabled() handles the + // null, but say so explicitly here: this is reached with an attacker-supplied quorumHash via + // CFinalCommitment::Verify(), so it must reject rather than trip a precondition. + if (util_params.m_base_index->pprev == nullptr || + !util_params.m_chainman.IsQuorumTypeEnabled(llmqType, util_params.m_base_index->pprev)) { return {}; } diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 8b10817938a6..b142730eeefc 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -1402,6 +1402,82 @@ void FuncTestMempoolReorg(TestChainSetup& setup) BOOST_CHECK_EQUAL(testPool.size(), 0U); } +// Regression test: a ProUpRev/ProUpReg invalidates every pending ProTx of the same masternode, and +// removeProTxKeyChangedConflicts() collects those into a std::set before removing any of +// them. When one conflicting TX is an in-mempool descendant of another, removeRecursive() on the +// ancestor also erases the descendant, so the descendant's txid in the snapshot no longer resolves. +// Dereferencing that stale iterator aborted the node; it must be skipped instead. +// +// Reachable from CTxMemPool::removeForBlock() (any block carrying such a ProTx). +void FuncTestMempoolProTxKeyChangedConflictChain(TestChainSetup& setup) +{ + auto& chainman = *Assert(setup.m_node.chainman.get()); + + auto utxos = BuildSimpleUtxoMap(setup.m_coinbase_txns); + const CScript scriptPayout = GetScriptForDestination(PKHash(setup.coinbaseKey.GetPubKey())); + + CKey ownerKey; + CBLSSecretKey operatorKey; + // Only the resulting proTxHash matters here; the registration never has to be mined because + // none of the paths under test consult the masternode list for a ProUpServ payload. + auto tx_reg = CreateProRegTx(chainman, utxos, 1, scriptPayout, setup.coinbaseKey, ownerKey, operatorKey); + const uint256 proTxHash = tx_reg.GetHash(); + + // Parent ProUpServ for that masternode. + auto tx_parent = CreateProUpServTx(chainman, utxos, proTxHash, operatorKey, 2, CScript(), setup.coinbaseKey); + BOOST_REQUIRE(!tx_parent.vout.empty()); + + // Child ProUpServ for the same masternode, spending the parent's first output. + FillableSigningProvider keystore; + BOOST_REQUIRE(keystore.AddKeyPubKey(setup.coinbaseKey, setup.coinbaseKey.GetPubKey())); + + CMutableTransaction tx_child; + tx_child.nVersion = 3; + tx_child.nType = TRANSACTION_PROVIDER_UPDATE_SERVICE; + tx_child.vin.emplace_back(COutPoint(tx_parent.GetHash(), 0)); + tx_child.vout.emplace_back(0, scriptPayout); // value assigned by the grind loop below + + CProUpServTx payload; + payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); + payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); + payload.proTxHash = proTxHash; + BOOST_REQUIRE_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:3"), NetInfoStatus::Success); + payload.inputsHash = CalcTxInputsHash(CTransaction(tx_child)); + payload.sig = operatorKey.Sign(::SerializeHash(payload), bls::bls_legacy_scheme); + SetTxPayload(tx_child, payload); + + // Grind the child's output value until the parent's txid sorts before the child's: the + // snapshot is an ordered set, so this makes the parent get removed first, taking the child + // with it, and only then is the child's now-stale txid revisited. Only the output value has to + // change -- inputsHash covers the inputs alone, so the payload and its BLS signature stay put, + // and re-signing replaces the scriptSig outright. + bool parent_sorts_first{false}; + for (CAmount fee = 1000; fee < 2000 && !parent_sorts_first; ++fee) { + tx_child.vout[0].nValue = tx_parent.vout[0].nValue - fee; + BOOST_REQUIRE(SignSignature(keystore, CTransaction(tx_parent), tx_child, 0, SIGHASH_ALL)); + parent_sorts_first = tx_parent.GetHash() < tx_child.GetHash(); + } + BOOST_REQUIRE(parent_sorts_first); + + // The revocation that invalidates both pending ProUpServ transactions. + auto tx_revoke = CreateProUpRevTx(chainman, utxos, proTxHash, operatorKey, setup.coinbaseKey); + + CTxMemPool testPool{MemPoolOptionsForTest(setup.m_node)}; + BOOST_REQUIRE(setup.m_node.dmnman); + testPool.ConnectManagers(setup.m_node.dmnman.get(), setup.m_node.llmq_ctx->isman.get()); + TestMemPoolEntryHelper entry; + LOCK2(cs_main, testPool.cs); + + testPool.addUnchecked(entry.FromTx(tx_parent)); + testPool.addUnchecked(entry.FromTx(tx_child)); + BOOST_CHECK_EQUAL(testPool.size(), 2U); + + // Pre-fix this aborted the process instead of returning. + std::vector block_txs{std::make_shared(tx_revoke)}; + testPool.removeForBlock(block_txs, chainman.ActiveChain().Height() + 1); + BOOST_CHECK_EQUAL(testPool.size(), 0U); +} + void FuncTestMempoolDualProregtx(TestChainSetup& setup) { auto& chainman = *Assert(setup.m_node.chainman.get()); @@ -1733,6 +1809,18 @@ BOOST_AUTO_TEST_CASE(test_mempool_reorg_basic) FuncTestMempoolReorg(setup); } +BOOST_AUTO_TEST_CASE(test_mempool_protx_key_changed_conflict_chain_legacy) +{ + TestChainDIP3Setup setup; + FuncTestMempoolProTxKeyChangedConflictChain(setup); +} + +BOOST_AUTO_TEST_CASE(test_mempool_protx_key_changed_conflict_chain_basic) +{ + TestChainV19Setup setup; + FuncTestMempoolProTxKeyChangedConflictChain(setup); +} + BOOST_AUTO_TEST_CASE(test_mempool_dual_proregtx_legacy) { TestChainDIP3Setup setup; diff --git a/src/test/evo_utils_tests.cpp b/src/test/evo_utils_tests.cpp index 3c892dda8128..4f70b0024821 100644 --- a/src/test/evo_utils_tests.cpp +++ b/src/test/evo_utils_tests.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include @@ -68,4 +70,30 @@ BOOST_FIXTURE_TEST_CASE(utils_IsQuorumTypeEnabled_tests_mainnet, TestingSetup) Test(m_node); } +// Regression: a quorum commitment naming the genesis block gives GetAllQuorumMembers() a base +// index whose pprev is null. That null used to be handed to IsQuorumTypeEnabled()'s +// gsl::not_null parameter, whose Expects() calls std::terminate() -- [[noreturn]] noexcept, so +// none of the catch(const std::exception&) handlers around special-tx processing could contain +// it, and every node connecting such a block died. It must be reported as "not enabled" instead. +BOOST_FIXTURE_TEST_CASE(genesis_quorum_base_null_pprev_is_safe, RegTestingSetup) +{ + const CBlockIndex* genesis = WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip()); + BOOST_REQUIRE(genesis != nullptr); + BOOST_REQUIRE_EQUAL(genesis->nHeight, 0); + BOOST_REQUIRE(genesis->pprev == nullptr); + + const auto llmq_type = Params().GetConsensus().llmqTypeChainLocks; + + // The sink itself, as reached via `pQuorumBaseBlockIndex->pprev`. Passed through a variable + // rather than a literal so this still compiles against the pre-fix gsl::not_null signature, + // where it terminated at runtime. + const CBlockIndex* const null_index = genesis->pprev; + BOOST_CHECK(!m_node.chainman->IsQuorumTypeEnabled(llmq_type, null_index)); + + // The GetAllQuorumMembers() path that CFinalCommitment::Verify() takes. + const llmq::UtilParameters util_params{*Assert(m_node.dmnman), *Assert(m_node.llmq_ctx)->qsnapman, + *Assert(m_node.chainman), genesis}; + BOOST_CHECK(llmq::utils::GetAllQuorumMembers(llmq_type, util_params).empty()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/llmq_invalid_type_tests.cpp b/src/test/llmq_invalid_type_tests.cpp new file mode 100644 index 000000000000..943299dbf2d2 --- /dev/null +++ b/src/test/llmq_invalid_type_tests.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +// An LLMQ type arriving in a P2P QSIGSHARE message is an unvalidated uint8_t. It used to reach +// CQuorumManager::GetQuorum() -> CQuorumBlockProcessor::HasMinedCommitment(), where indexing +// mapHasMinedCommitmentCache with std::map::operator[] inserted a default-constructed +// Uint256LruHashMap. Its default MaxSize is 0, and unordered_lru_cache's constructor asserts +// maxSize != 0, so any of the ~250 unregistered type values aborted the node. +// +// These caches are only ever seeded by InitQuorumsCache() with the LLMQ types in the active +// chain's consensus params, so every lookup keyed by untrusted input must use find() rather than +// operator[]. The message handler now rejects unregistered types up front as well. + +BOOST_AUTO_TEST_SUITE(llmq_invalid_type_tests) + +// Not a named enumerator, and never present in any chain's consensus params. +static constexpr Consensus::LLMQType UNKNOWN_LLMQ_TYPE{static_cast(0)}; +// LLMQ_25_67. A real enumerator, but registered on testnet only, so it is unknown under regtest. +static constexpr Consensus::LLMQType UNREGISTERED_LLMQ_TYPE{Consensus::LLMQType::LLMQ_25_67}; + +BOOST_FIXTURE_TEST_CASE(init_quorums_cache_does_not_seed_unknown_types, BasicTestingSetup) +{ + std::map> cache; + llmq::utils::InitQuorumsCache(cache, Params().GetConsensus()); + BOOST_REQUIRE(!cache.empty()); + + BOOST_REQUIRE(!Params().GetLLMQ(UNKNOWN_LLMQ_TYPE).has_value()); + BOOST_CHECK(cache.find(UNKNOWN_LLMQ_TYPE) == cache.end()); + + // Every seeded type has a non-zero capacity; anything else would have to be + // default-constructed, which is exactly what aborts. + for (const auto& llmq : Params().GetConsensus().llmqs) { + auto it = cache.find(llmq.type); + BOOST_REQUIRE(it != cache.end()); + BOOST_CHECK_GT(it->second.max_size(), 0U); + } +} + +// Crash site. Pre-fix this aborted with `Assertion failed: (_maxSize != 0)`; it must report that +// there is no mined commitment instead. +BOOST_FIXTURE_TEST_CASE(has_mined_commitment_unknown_llmq_type_is_safe, RegTestingSetup) +{ + const auto& qbp = *Assert(Assert(m_node.llmq_ctx)->quorum_block_processor); + const uint256 tip_hash = WITH_LOCK(::cs_main, return Assert(m_node.chainman->ActiveTip())->GetBlockHash()); + + BOOST_REQUIRE(!Params().GetLLMQ(UNKNOWN_LLMQ_TYPE).has_value()); + BOOST_REQUIRE(!Params().GetLLMQ(UNREGISTERED_LLMQ_TYPE).has_value()); + + BOOST_CHECK(!qbp.HasMinedCommitment(UNKNOWN_LLMQ_TYPE, tip_hash)); + BOOST_CHECK(!qbp.HasMinedCommitment(UNREGISTERED_LLMQ_TYPE, tip_hash)); +} + +// The exact call ProcessMessageSigShare() makes with wire-supplied values: LookupBlockIndex() +// succeeds for a real block hash, so HasQuorum() -> HasMinedCommitment() runs with the +// attacker-chosen LLMQ type. Pre-fix this aborted; it must return nullptr instead. +BOOST_FIXTURE_TEST_CASE(get_quorum_unknown_llmq_type_is_safe, RegTestingSetup) +{ + const auto& qman = *Assert(Assert(m_node.llmq_ctx)->qman); + const uint256 tip_hash = WITH_LOCK(::cs_main, return Assert(m_node.chainman->ActiveTip())->GetBlockHash()); + + BOOST_REQUIRE(!Params().GetLLMQ(UNKNOWN_LLMQ_TYPE).has_value()); + BOOST_REQUIRE(!Params().GetLLMQ(UNREGISTERED_LLMQ_TYPE).has_value()); + + BOOST_CHECK(qman.GetQuorum(UNKNOWN_LLMQ_TYPE, tip_hash) == nullptr); + BOOST_CHECK(qman.GetQuorum(UNREGISTERED_LLMQ_TYPE, tip_hash) == nullptr); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 8524d6a07e71..7aeb75ab0c90 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -1017,8 +1017,15 @@ void CTxMemPool::removeProTxKeyChangedConflicts(const CTransaction &tx, const ui } } for (const auto& txHash : conflictingTxs) { - auto& tx = mapTx.find(txHash)->GetTx(); - removeRecursive(tx, MemPoolRemovalReason::CONFLICT); + // `conflictingTxs` is a snapshot taken before any removal. A conflicting TX may be a + // descendant of an earlier one, in which case removeRecursive() has already erased it and + // `mapTx.find()` returns end(). Dereferencing that iterator would abort the node, so skip + // entries that are already gone (same guard the other removeProTx*Conflicts helpers use). + auto txit = mapTx.find(txHash); + if (txit == mapTx.end()) { + continue; + } + removeRecursive(txit->GetTx(), MemPoolRemovalReason::CONFLICT); } } diff --git a/src/validation.cpp b/src/validation.cpp index bd99df8bc8d6..d6d48d6b9e1f 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5723,10 +5723,17 @@ bool ChainstateManager::IsSnapshotActive() const } bool ChainstateManager::IsQuorumTypeEnabled(const Consensus::LLMQType llmqType, - gsl::not_null pindexPrev, + const CBlockIndex* pindexPrev, std::optional optDIP0024IsActive, std::optional optHaveDIP0024Quorums) const { + // A parentless block index (i.e. genesis) has no prior height at which any LLMQ type could + // have activated, so nothing is enabled. This matches the behaviour before pindexPrev was + // hardened to gsl::not_null, when DeploymentActiveAfter(nullptr, ...) simply returned false. + if (pindexPrev == nullptr) { + return false; + } + constexpr int TESTNET_LLMQ_25_67_ACTIVATION_HEIGHT = 847000; const bool fDIP0024IsActive{optDIP0024IsActive.value_or( diff --git a/src/validation.h b/src/validation.h index 3fb064739d83..bf8c321fac78 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1091,7 +1091,12 @@ class ChainstateManager //! ResizeCoinsCaches() as needed. void MaybeRebalanceCaches() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool IsQuorumTypeEnabled(const Consensus::LLMQType llmqType, gsl::not_null pindexPrev, + //! pindexPrev may be nullptr -- genesis has no parent, and a block index with no parent can + //! never be a valid quorum base -- in which case no LLMQ type is enabled and this returns + //! false. Do not tighten this to gsl::not_null: attacker-supplied quorum hashes reach it as + //! `pindex->pprev`, and a not_null precondition turns that into a process abort rather than a + //! rejection. + bool IsQuorumTypeEnabled(const Consensus::LLMQType llmqType, const CBlockIndex* pindexPrev, std::optional optDIP0024IsActive = std::nullopt, std::optional optHaveDIP0024Quorums = std::nullopt) const;