From 31142da98c8147b0d092d9fa0eff29def5a13b10 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 29 Jun 2026 20:46:23 -0500 Subject: [PATCH 1/9] llmq: harden DKG message-intake against unauthenticated retention and crashes Harden the pushed DKG message path (QCONTRIB/QCOMPLAINT/QJUSTIFICATION/QPCOMMITMENT): - Require the sending peer to be MNAuth-verified (qwatch is unauthenticated and does not bypass this). - Reject oversized DKG payloads (per-type MaxDKGMessageSize) before deserialization/retention. - Structurally pre-validate (param-only checks, on a copy) before retention. - Never feed an invalid BLS signature to CBLSSignature::AggregateInsecure(): the batch verifier skips invalid sigs and per-type PreVerifyMessage rejects them. --- src/Makefile.am | 1 + src/active/dkgsessionhandler.cpp | 25 +++ src/llmq/dkgmessages.h | 188 +++++++++++++++++++++ src/llmq/dkgsession.cpp | 25 +++ src/llmq/dkgsession.h | 169 +----------------- src/llmq/dkgsessionmgr.cpp | 103 +++++++++++ test/functional/feature_llmq_dkg_intake.py | 149 ++++++++++++++++ test/functional/test_runner.py | 1 + 8 files changed, 493 insertions(+), 168 deletions(-) create mode 100644 src/llmq/dkgmessages.h create mode 100755 test/functional/feature_llmq_dkg_intake.py diff --git a/src/Makefile.am b/src/Makefile.am index ff33c240b095..55ef13001b68 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -277,6 +277,7 @@ BITCOIN_CORE_H = \ llmq/commitment.h \ llmq/context.h \ llmq/debug.h \ + llmq/dkgmessages.h \ llmq/dkgsession.h \ llmq/dkgsessionhandler.h \ llmq/dkgsessionmgr.h \ diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 1c74095ad7d2..f83f6826ccba 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -275,6 +275,18 @@ std::set BatchVerifyMessageSigs(CDKGSession& session, const std::vector< continue; } + // An invalid signature must never reach AggregateInsecure(), which asserts + // that both operands are valid. Mark the sender bad and skip it instead of + // aggregating it. This guard is mandatory: it covers every + // message type the batch verifier is instantiated for, including those whose + // per-message PreVerifyMessage does not (yet) reject invalid signatures. + // Note: 'first' below tracks the first *accumulated* (valid) signature, not + // the first *examined* message, so skipping leading invalid sigs is safe. + if (!msg->sig.IsValid()) { + ret.emplace(nodeId); + continue; + } + if (first) { aggSig = msg->sig; } else { @@ -298,6 +310,12 @@ std::set BatchVerifyMessageSigs(CDKGSession& session, const std::vector< messageHashes.emplace_back(msgHash); } if (!revertToSingleVerification) { + if (pubKeys.empty()) { + // Every message had an unknown member or invalid signature; all such + // senders are already in ret. VerifyInsecureAggregated() asserts that + // the pubkey/hash spans are non-empty, so bail out here. + return ret; + } if (aggSig.VerifyInsecureAggregated(pubKeys, messageHashes)) { // all good return ret; @@ -322,6 +340,13 @@ std::set BatchVerifyMessageSigs(CDKGSession& session, const std::vector< } auto member = session.GetMember(msg->proTxHash); + if (member == nullptr || !msg->sig.IsValid()) { + // Examined messages with these properties are already in ret, but the + // early break on a duplicate hash above can leave some unexamined. + // Stay defensive: never dereference a null member or verify an invalid sig. + ret.emplace(nodeId); + continue; + } bool valid = msg->sig.VerifyInsecure(member->dmn->pdmnState->pubKeyOperator.Get(), msg->GetSignHash()); if (!valid) { ret.emplace(nodeId); diff --git a/src/llmq/dkgmessages.h b/src/llmq/dkgmessages.h new file mode 100644 index 000000000000..b3421c88ba75 --- /dev/null +++ b/src/llmq/dkgmessages.h @@ -0,0 +1,188 @@ +// Copyright (c) 2018-2025 The Dash Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_LLMQ_DKGMESSAGES_H +#define BITCOIN_LLMQ_DKGMESSAGES_H + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace llmq { +class CDKGContribution +{ +public: + Consensus::LLMQType llmqType; + uint256 quorumHash; + uint256 proTxHash; + BLSVerificationVectorPtr vvec; + std::shared_ptr> contributions; + CBLSSignature sig; + +public: + template + inline void SerializeWithoutSig(Stream& s) const + { + s << ToUnderlying(llmqType); + s << quorumHash; + s << proTxHash; + s << *vvec; + s << *contributions; + } + template + inline void Serialize(Stream& s) const + { + SerializeWithoutSig(s); + s << sig; + } + template + inline void Unserialize(Stream& s) + { + std::vector tmp1; + CBLSIESMultiRecipientObjects tmp2; + + s >> llmqType; + s >> quorumHash; + s >> proTxHash; + s >> tmp1; + s >> tmp2; + s >> sig; + + vvec = std::make_shared>(std::move(tmp1)); + contributions = std::make_shared>(std::move(tmp2)); + } + + [[nodiscard]] uint256 GetSignHash() const + { + CHashWriter hw(SER_GETHASH, 0); + SerializeWithoutSig(hw); + hw << CBLSSignature(); + return hw.GetHash(); + } +}; + +class CDKGComplaint +{ +public: + Consensus::LLMQType llmqType{Consensus::LLMQType::LLMQ_NONE}; + uint256 quorumHash; + uint256 proTxHash; + std::vector badMembers; + std::vector complainForMembers; + CBLSSignature sig; + +public: + CDKGComplaint() = default; + explicit CDKGComplaint(const Consensus::LLMQParams& params) : + badMembers((size_t)params.size), complainForMembers((size_t)params.size) {}; + + SERIALIZE_METHODS(CDKGComplaint, obj) + { + READWRITE( + obj.llmqType, + obj.quorumHash, + obj.proTxHash, + DYNBITSET(obj.badMembers), + DYNBITSET(obj.complainForMembers), + obj.sig + ); + } + + [[nodiscard]] uint256 GetSignHash() const + { + CDKGComplaint tmp(*this); + tmp.sig = CBLSSignature(); + return ::SerializeHash(tmp); + } +}; + +class CDKGJustification +{ +public: + Consensus::LLMQType llmqType; + uint256 quorumHash; + uint256 proTxHash; + struct Contribution { + uint32_t index; + CBLSSecretKey key; + SERIALIZE_METHODS(Contribution, obj) + { + READWRITE(obj.index, obj.key); + } + }; + std::vector contributions; + CBLSSignature sig; + +public: + SERIALIZE_METHODS(CDKGJustification, obj) + { + READWRITE(obj.llmqType, obj.quorumHash, obj.proTxHash, obj.contributions, obj.sig); + } + + [[nodiscard]] uint256 GetSignHash() const + { + CDKGJustification tmp(*this); + tmp.sig = CBLSSignature(); + return ::SerializeHash(tmp); + } +}; + +// each member commits to a single set of valid members with this message +// then each node aggregate all received premature commitments +// into a single CFinalCommitment, which is only valid if +// enough (>=minSize) premature commitments were aggregated +class CDKGPrematureCommitment +{ +public: + Consensus::LLMQType llmqType{Consensus::LLMQType::LLMQ_NONE}; + uint256 quorumHash; + uint256 proTxHash; + std::vector validMembers; + + CBLSPublicKey quorumPublicKey; + uint256 quorumVvecHash; + + CBLSSignature quorumSig; // threshold sig share of quorumHash+validMembers+pubKeyHash+vvecHash + CBLSSignature sig; // single member sig of quorumHash+validMembers+pubKeyHash+vvecHash + +public: + CDKGPrematureCommitment() = default; + explicit CDKGPrematureCommitment(const Consensus::LLMQParams& params) : + validMembers((size_t)params.size) {}; + + [[nodiscard]] int CountValidMembers() const + { + return int(std::count(validMembers.begin(), validMembers.end(), true)); + } + +public: + SERIALIZE_METHODS(CDKGPrematureCommitment, obj) + { + READWRITE( + obj.llmqType, + obj.quorumHash, + obj.proTxHash, + DYNBITSET(obj.validMembers), + obj.quorumPublicKey, + obj.quorumVvecHash, + obj.quorumSig, + obj.sig + ); + } + + [[nodiscard]] uint256 GetSignHash() const + { + return BuildCommitmentHash(llmqType, quorumHash, validMembers, quorumPublicKey, quorumVvecHash); + } +}; +} // namespace llmq + +#endif // BITCOIN_LLMQ_DKGMESSAGES_H diff --git a/src/llmq/dkgsession.cpp b/src/llmq/dkgsession.cpp index db1cb05311c4..85ae65b40d4e 100644 --- a/src/llmq/dkgsession.cpp +++ b/src/llmq/dkgsession.cpp @@ -169,6 +169,15 @@ bool CDKGSession::PreVerifyMessage(const CDKGContribution& qc, bool& retBan) con return false; } + // Reject a structurally-invalid (e.g. all-zero) signature here. This is a cheap + // validity check, not the batched signature verification; it ensures the message + // never reaches CBLSSignature::AggregateInsecure(), which asserts validity. + if (!qc.sig.IsValid()) { + logger.Batch("invalid contribution signature"); + retBan = true; + return false; + } + if (qc.contributions->blobs.size() != members.size()) { logger.Batch("invalid contributions count"); retBan = true; @@ -299,6 +308,14 @@ bool CDKGSession::PreVerifyMessage(const CDKGComplaint& qc, bool& retBan) const return false; } + // Cheap validity check (not the batched signature verification): reject a + // structurally-invalid signature before it can reach AggregateInsecure(). + if (!qc.sig.IsValid()) { + logger.Batch("invalid complaint signature"); + retBan = true; + return false; + } + if (qc.badMembers.size() != (size_t)params.size) { logger.Batch("invalid badMembers bitset size"); retBan = true; @@ -403,6 +420,14 @@ bool CDKGSession::PreVerifyMessage(const CDKGJustification& qj, bool& retBan) co return false; } + // Cheap validity check (not the batched signature verification): reject a + // structurally-invalid signature before it can reach AggregateInsecure(). + if (!qj.sig.IsValid()) { + logger.Batch("invalid justification signature"); + retBan = true; + return false; + } + if (qj.contributions.empty()) { logger.Batch("justification with no contributions"); retBan = true; diff --git a/src/llmq/dkgsession.h b/src/llmq/dkgsession.h index 95529b2bdde6..c7f507537c9c 100644 --- a/src/llmq/dkgsession.h +++ b/src/llmq/dkgsession.h @@ -5,7 +5,7 @@ #ifndef BITCOIN_LLMQ_DKGSESSION_H #define BITCOIN_LLMQ_DKGSESSION_H -#include +#include #include #include @@ -39,173 +39,6 @@ class CQuorumSnapshotManager; } // namespace llmq namespace llmq { -class CDKGContribution -{ -public: - Consensus::LLMQType llmqType; - uint256 quorumHash; - uint256 proTxHash; - BLSVerificationVectorPtr vvec; - std::shared_ptr> contributions; - CBLSSignature sig; - -public: - template - inline void SerializeWithoutSig(Stream& s) const - { - s << ToUnderlying(llmqType); - s << quorumHash; - s << proTxHash; - s << *vvec; - s << *contributions; - } - template - inline void Serialize(Stream& s) const - { - SerializeWithoutSig(s); - s << sig; - } - template - inline void Unserialize(Stream& s) - { - std::vector tmp1; - CBLSIESMultiRecipientObjects tmp2; - - s >> llmqType; - s >> quorumHash; - s >> proTxHash; - s >> tmp1; - s >> tmp2; - s >> sig; - - vvec = std::make_shared>(std::move(tmp1)); - contributions = std::make_shared>(std::move(tmp2)); - } - - [[nodiscard]] uint256 GetSignHash() const - { - CHashWriter hw(SER_GETHASH, 0); - SerializeWithoutSig(hw); - hw << CBLSSignature(); - return hw.GetHash(); - } -}; - -class CDKGComplaint -{ -public: - Consensus::LLMQType llmqType{Consensus::LLMQType::LLMQ_NONE}; - uint256 quorumHash; - uint256 proTxHash; - std::vector badMembers; - std::vector complainForMembers; - CBLSSignature sig; - -public: - CDKGComplaint() = default; - explicit CDKGComplaint(const Consensus::LLMQParams& params) : - badMembers((size_t)params.size), complainForMembers((size_t)params.size) {}; - - SERIALIZE_METHODS(CDKGComplaint, obj) - { - READWRITE( - obj.llmqType, - obj.quorumHash, - obj.proTxHash, - DYNBITSET(obj.badMembers), - DYNBITSET(obj.complainForMembers), - obj.sig - ); - } - - [[nodiscard]] uint256 GetSignHash() const - { - CDKGComplaint tmp(*this); - tmp.sig = CBLSSignature(); - return ::SerializeHash(tmp); - } -}; - -class CDKGJustification -{ -public: - Consensus::LLMQType llmqType; - uint256 quorumHash; - uint256 proTxHash; - struct Contribution { - uint32_t index; - CBLSSecretKey key; - SERIALIZE_METHODS(Contribution, obj) - { - READWRITE(obj.index, obj.key); - } - }; - std::vector contributions; - CBLSSignature sig; - -public: - SERIALIZE_METHODS(CDKGJustification, obj) - { - READWRITE(obj.llmqType, obj.quorumHash, obj.proTxHash, obj.contributions, obj.sig); - } - - [[nodiscard]] uint256 GetSignHash() const - { - CDKGJustification tmp(*this); - tmp.sig = CBLSSignature(); - return ::SerializeHash(tmp); - } -}; - -// each member commits to a single set of valid members with this message -// then each node aggregate all received premature commitments -// into a single CFinalCommitment, which is only valid if -// enough (>=minSize) premature commitments were aggregated -class CDKGPrematureCommitment -{ -public: - Consensus::LLMQType llmqType{Consensus::LLMQType::LLMQ_NONE}; - uint256 quorumHash; - uint256 proTxHash; - std::vector validMembers; - - CBLSPublicKey quorumPublicKey; - uint256 quorumVvecHash; - - CBLSSignature quorumSig; // threshold sig share of quorumHash+validMembers+pubKeyHash+vvecHash - CBLSSignature sig; // single member sig of quorumHash+validMembers+pubKeyHash+vvecHash - -public: - CDKGPrematureCommitment() = default; - explicit CDKGPrematureCommitment(const Consensus::LLMQParams& params) : - validMembers((size_t)params.size) {}; - - [[nodiscard]] int CountValidMembers() const - { - return int(std::count(validMembers.begin(), validMembers.end(), true)); - } - -public: - SERIALIZE_METHODS(CDKGPrematureCommitment, obj) - { - READWRITE( - obj.llmqType, - obj.quorumHash, - obj.proTxHash, - DYNBITSET(obj.validMembers), - obj.quorumPublicKey, - obj.quorumVvecHash, - obj.quorumSig, - obj.sig - ); - } - - [[nodiscard]] uint256 GetSignHash() const - { - return BuildCommitmentHash(llmqType, quorumHash, validMembers, quorumPublicKey, quorumVvecHash); - } -}; - class CDKGMember { public: diff --git a/src/llmq/dkgsessionmgr.cpp b/src/llmq/dkgsessionmgr.cpp index 0c9a74f0570b..aa8be88b6037 100644 --- a/src/llmq/dkgsessionmgr.cpp +++ b/src/llmq/dkgsessionmgr.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -29,6 +30,86 @@ static const std::string DB_VVEC = "qdkg_V"; static const std::string DB_SKCONTRIB = "qdkg_S"; static const std::string DB_ENC_CONTRIB = "qdkg_E"; +namespace { +// Upper bound on the serialized size of a well-formed DKG message of the given +// type for the given quorum params. Used to reject oversized payloads at intake +// before any deserialization or retention, which closes the low-cost memory +// amplification window (a legitimate message is bounded by quorum params, far +// below the 3 MiB transport cap). Generous slack is added and the result is +// clamped to a hard ceiling so a future params change can never silently re-open +// the full transport window. +size_t MaxDKGMessageSize(std::string_view msg_type, const Consensus::LLMQParams& params) +{ + constexpr size_t COMPACT = 5; // max CompactSize for any realistic count + constexpr size_t PREFIX = 1 + 32 + 32; // llmqType + quorumHash + proTxHash + constexpr size_t PUBKEY = BLS_CURVE_PUBKEY_SIZE; // 48 + constexpr size_t SIG = BLS_CURVE_SIG_SIZE; // 96 + constexpr size_t SECKEY = BLS_CURVE_SECKEY_SIZE; // 32 + constexpr size_t BLOB = COMPACT + 128; // encrypted seckey blob, generous + constexpr size_t SLACK = 1024; + constexpr size_t HARD_CEILING = size_t{1} << 20; // 1 MiB + + const size_t size = params.size > 0 ? static_cast(params.size) : 0; + const size_t threshold = params.threshold > 0 ? static_cast(params.threshold) : 0; + + size_t cap = 0; + if (msg_type == NetMsgType::QCONTRIB) { + // llmqType/quorumHash/proTxHash + vvec + contributions(IES) + sig + cap = PREFIX + (COMPACT + threshold * PUBKEY) + (PUBKEY + 32 + COMPACT + size * BLOB) + SIG; + } else if (msg_type == NetMsgType::QJUSTIFICATION) { + // ... + contributions(index u32 + seckey) + sig + cap = PREFIX + (COMPACT + size * (4 + SECKEY)) + SIG; + } else if (msg_type == NetMsgType::QCOMPLAINT) { + // ... + 2 dynamic bitsets (badMembers, complainForMembers) + sig + cap = PREFIX + 2 * (COMPACT + (size + 7) / 8) + SIG; + } else if (msg_type == NetMsgType::QPCOMMITMENT) { + // ... + validMembers bitset + quorumPublicKey + quorumVvecHash + quorumSig + sig + cap = PREFIX + (COMPACT + (size + 7) / 8) + PUBKEY + 32 + 2 * SIG; + } else { + return HARD_CEILING; + } + cap += SLACK; + return cap < HARD_CEILING ? cap : HARD_CEILING; +} + +// Cheap, param-only structural validation of a pushed DKG message, run at intake +// before retention. Deserializes a COPY of the payload (leaving the caller's bytes +// intact for the pending queue and its inventory hash) and checks only invariants +// derived from quorum params: no member-list lookup and no signature verification, +// which remain on the DKG worker thread. Deserializing the copy does decompress the +// BLS points carried in the payload, but that work is bounded by the size cap applied +// just before this check. Rejects malformed or wrong-shaped payloads before retention. +bool CheckDKGMessageStructure(std::string_view msg_type, const CDataStream& vRecv, const Consensus::LLMQParams& params) +{ + const size_t size = params.size > 0 ? static_cast(params.size) : 0; + const size_t threshold = params.threshold > 0 ? static_cast(params.threshold) : 0; + try { + CDataStream s(vRecv); // copy; deserialization does not advance the caller's stream + if (msg_type == NetMsgType::QCONTRIB) { + CDKGContribution qc; + s >> qc; + return qc.vvec != nullptr && qc.vvec->size() == threshold && + qc.contributions != nullptr && qc.contributions->blobs.size() == size; + } else if (msg_type == NetMsgType::QCOMPLAINT) { + CDKGComplaint qc; + s >> qc; + return qc.badMembers.size() == size && qc.complainForMembers.size() == size; + } else if (msg_type == NetMsgType::QJUSTIFICATION) { + CDKGJustification qj; + s >> qj; + return qj.contributions.size() <= size; + } else if (msg_type == NetMsgType::QPCOMMITMENT) { + CDKGPrematureCommitment qc; + s >> qc; + return qc.validMembers.size() == size; + } + return false; + } catch (const std::exception&) { + return false; + } +} +} // anonymous namespace + CDKGSessionManager::CDKGSessionManager(CDeterministicMNManager& dmnman, CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, const CSporkManager& sporkman, const util::DbWrapperParams& db_params, bool quorums_watch) : @@ -104,6 +185,14 @@ MessageProcessingResult CDKGSessionManager::ProcessMessage(CNode& pfrom, bool is return MisbehavingError{10}; } + // Pushed DKG messages (QCONTRIB/QCOMPLAINT/QJUSTIFICATION/QPCOMMITMENT) retain + // attacker-controlled payloads, so they must originate from an MNAuth-verified + // masternode. qwatch is unauthenticated (any peer can set it via QWATCH) and is + // only meaningful for pull/observation paths; it must not bypass this gate. + if (pfrom.GetVerifiedProRegTxHash().IsNull()) { + return MisbehavingError{10, "DKG message from non-verified peer"}; + } + if (vRecv.empty()) { return MisbehavingError{100}; } @@ -164,6 +253,20 @@ MessageProcessingResult CDKGSessionManager::ProcessMessage(CNode& pfrom, bool is } assert(quorumIndex != -1); + + // Reject oversized payloads before any deserialization or retention. A + // well-formed DKG message is bounded by quorum params; anything larger is an + // amplification attempt against the per-peer pending queue. + if (vRecv.size() > MaxDKGMessageSize(msg_type, llmq_params)) { + return MisbehavingError{100, "oversized DKG message"}; + } + + // Cheap structural pre-validation before retention. Validates a copy so the + // original bytes (and their inventory hash) are preserved for the worker. + if (!CheckDKGMessageStructure(msg_type, vRecv, llmq_params)) { + return MisbehavingError{100, "malformed DKG message"}; + } + WITH_LOCK(cs_indexedQuorumsCache, indexedQuorumsCache[llmqType].insert(quorumHash, quorumIndex)); return Assert(dkgSessionHandlers.at({llmqType, quorumIndex}))->ProcessMessage(pfrom.GetId(), msg_type, vRecv); } diff --git a/test/functional/feature_llmq_dkg_intake.py b/test/functional/feature_llmq_dkg_intake.py new file mode 100755 index 000000000000..168c94eead38 --- /dev/null +++ b/test/functional/feature_llmq_dkg_intake.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# 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. +""" +feature_llmq_dkg_intake.py + +Adversarial P2P tests for DKG message-intake hardening: + - pushed DKG messages (qcontrib/qcomplaint/qjustify/qpcommit) from a peer that is + not MNAuth-verified are rejected before retention. + - oversized DKG payloads are rejected (before deserialization / retention) even + from a verified peer. + - structural pre-validation: malformed DKG payloads (valid quorum prefix, garbage + body) are rejected before retention even from a verified peer. + +The node must not crash; the sending peer must be scored (Misbehaving). +""" + +from test_framework.messages import ser_uint256 +from test_framework.p2p import P2PInterface +from test_framework.test_framework import DashTestFramework +from test_framework.util import wait_until_helper + +LLMQ_TEST = 100 + +# A masternode protx/operator-pubkey pair accepted by the regtest-only `mnauth` +# debug RPC, used to mark a P2P connection as MNAuth-verified without BLS signing. +FAKE_PROTX = "cecf37bf0ec05d2d22cb8227f88074bb882b94cd2081ba318a5a444b1b15b9fd" +FAKE_PUBKEY = "8e7afdb849e5e2a085b035b62e21c0940c753f2d4501325743894c37162f287bccaffbedd60c36581dabbf127a22e43f" + +DKG_PUSH_TYPES = [b"qcontrib", b"qcomplaint", b"qjustify", b"qpcommit"] + + +class msg_dkg_raw: + """A DKG push message carrying an arbitrary raw payload (for adversarial intake tests).""" + __slots__ = ("msgtype", "payload") + + def __init__(self, msgtype, payload=b""): + self.msgtype = msgtype + self.payload = payload + + def serialize(self): + return self.payload + + def __repr__(self): + return "msg_dkg_raw(type=%s, len=%d)" % (self.msgtype, len(self.payload)) + + +def get_p2p_id(node): + def get_id(): + for p in node.getpeerinfo(): + for p2p in node.p2ps: + if p["subver"] == p2p.strSubVer: + return p["id"] + return None + wait_until_helper(lambda: get_id() is not None, timeout=10) + return get_id() + + +def wait_for_banscore(node, peer_id, expected_score): + def get_score(): + for peer in node.getpeerinfo(): + if peer["id"] == peer_id: + return peer["banscore"] + return None + wait_until_helper(lambda: get_score() == expected_score, timeout=10) + + +class DkgIntakeTest(DashTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + + def set_test_params(self): + # -whitelist keeps the adversarial peer connected even after it crosses the + # discouragement threshold, so banscore stays observable for the score==100 cases. + # -debug=net surfaces the Misbehaving reason strings in debug.log. + extra_args = [["-whitelist=127.0.0.1", "-debug=net", "-deprecatedrpc=banscore"]] * 4 + self.set_dash_test_params(4, 3, extra_args=extra_args) + + def quorum_hash_prefix(self): + # llmqType (1 byte) + quorumHash (32 bytes, little-endian) -- the on-wire prefix + # shared by every DKG message, used so oversized/malformed payloads resolve to a + # real in-progress quorum and reach the size/structural checks. + return bytes([LLMQ_TEST]) + ser_uint256(int(self.quorum_hash, 16)) + + def add_verified_peer(self, node): + peer = node.add_p2p_connection(P2PInterface()) + peer_id = get_p2p_id(node) + assert node.mnauth(peer_id, FAKE_PROTX, FAKE_PUBKEY) + return peer, peer_id + + def run_test(self): + node0 = self.nodes[0] + node0.sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + + # Mine a quorum so we have a quorumHash that resolves to a valid DKG base block. + self.quorum_hash = self.mine_quorum() + + # Target an active masternode -- the realistic victim of these messages. + mn_node = self.mninfo[0].get_node(self) + + self.test_unverified_sender_rejected(mn_node) + self.test_oversized_rejected(mn_node) + self.test_malformed_rejected(mn_node) + + def test_unverified_sender_rejected(self, node): + self.log.info("Pushed DKG messages from a non-verified peer are rejected (Misbehaving 10 each)") + peer = node.add_p2p_connection(P2PInterface()) + peer_id = get_p2p_id(node) + wait_for_banscore(node, peer_id, 0) + score = 0 + for msgtype in DKG_PUSH_TYPES: + with node.assert_debug_log(["DKG message from non-verified peer"]): + peer.send_message(msg_dkg_raw(msgtype, self.quorum_hash_prefix())) + peer.sync_with_ping() + score += 10 + wait_for_banscore(node, peer_id, score) + node.disconnect_p2ps() + + def test_oversized_rejected(self, node): + self.log.info("Oversized DKG payloads are rejected even from a verified peer (Misbehaving 100)") + peer, peer_id = self.add_verified_peer(node) + wait_for_banscore(node, peer_id, 0) + # >1 MiB clears the hard ceiling regardless of quorum params, and stays under the + # 3 MiB transport cap so the message is delivered to the handler. + payload = self.quorum_hash_prefix() + b"\x00" * (1024 * 1024 + 4096) + with node.assert_debug_log(["oversized DKG message"]): + peer.send_message(msg_dkg_raw(b"qcontrib", payload)) + peer.sync_with_ping() + wait_for_banscore(node, peer_id, 100) + node.disconnect_p2ps() + + def test_malformed_rejected(self, node): + self.log.info("Malformed DKG payloads are rejected even from a verified peer (Misbehaving 100)") + peer, peer_id = self.add_verified_peer(node) + wait_for_banscore(node, peer_id, 0) + # Valid llmqType + quorumHash prefix, then too few bytes to deserialize a + # CDKGContribution -> structural pre-validation rejects it before retention. + payload = self.quorum_hash_prefix() + b"\x00\x00\x00\x00" + with node.assert_debug_log(["malformed DKG message"]): + peer.send_message(msg_dkg_raw(b"qcontrib", payload)) + peer.sync_with_ping() + wait_for_banscore(node, peer_id, 100) + node.disconnect_p2ps() + + +if __name__ == '__main__': + DkgIntakeTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index e6f06a9f2f17..ecb2a49a10eb 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -137,6 +137,7 @@ 'feature_llmq_evo.py', # NOTE: needs dash_hash to pass 'feature_llmq_is_cl_conflicts.py', # NOTE: needs dash_hash to pass 'feature_llmq_dkgerrors.py', # NOTE: needs dash_hash to pass + 'feature_llmq_dkg_intake.py', # NOTE: needs dash_hash to pass 'feature_llmq_singlenode.py', # NOTE: needs dash_hash to pass 'feature_dip4_coinbasemerkleroots.py', # NOTE: needs dash_hash to pass 'feature_mnehf.py', # NOTE: needs dash_hash to pass From 1132a25bae89c728f93233d2f19572055b07617a Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 29 Jun 2026 20:46:24 -0500 Subject: [PATCH 2/9] bls: guard AsyncVerifyContributionShare against a null verification vector A validly-signed DKG self-complaint + justification can reach CBLSWorker::AsyncVerifyContributionShare() with a null BLSVerificationVectorPtr (when the member's contribution was never received), which the singular overload dereferenced with no null check, crashing observers/non-member masternodes. Treat a null vvec as a failed verification, matching the plural path. Includes a bls_tests regression case. --- src/bls/bls_worker.cpp | 6 +++++- src/test/bls_tests.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/bls/bls_worker.cpp b/src/bls/bls_worker.cpp index 3191e95338e4..62762ebd2a91 100644 --- a/src/bls/bls_worker.cpp +++ b/src/bls/bls_worker.cpp @@ -711,7 +711,11 @@ std::future CBLSWorker::AsyncVerifyContributionShare(const CBLSId& forId, const BLSVerificationVectorPtr& vvec, const CBLSSecretKey& skContribution) { - if (!forId.IsValid() || !VerifyVerificationVector(*vvec)) { + // vvec may be null when the verification vector for that member was never + // received (e.g. a non-member observer that did not get the member's QCONTRIB). + // Dereferencing it here is a remote-triggerable crash; treat a missing vvec as a + // failed verification, mirroring the null check in VerifyVerificationVectors(). + if (!forId.IsValid() || vvec == nullptr || !VerifyVerificationVector(*vvec)) { auto p = BuildFutureDoneCallback(); p.first(false); return std::move(p.second); diff --git a/src/test/bls_tests.cpp b/src/test/bls_tests.cpp index e6232668be57..08b05a6332fb 100644 --- a/src/test/bls_tests.cpp +++ b/src/test/bls_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -473,6 +474,41 @@ BOOST_AUTO_TEST_CASE(bls_threshold_signature_tests) FuncThresholdSignature(false); } +// Regression test: a DKG justification path can reach +// CBLSWorker::AsyncVerifyContributionShare() with a null verification vector when +// the corresponding member's contribution was never received (e.g. on a non-member +// observer). The singular overload dereferenced *vvec with no null check and aborted +// the process; it must instead treat a missing vvec as a failed verification, like +// the plural VerifyVerificationVectors() path. A *valid* id is required so the guard +// does not short-circuit before reaching the (previously) unguarded dereference. +void FuncVerifyContributionShareNullVvec(const bool legacy_scheme) +{ + bls::bls_legacy_scheme.store(legacy_scheme); + + CBLSWorker worker; + worker.Start(); + + const CBLSId id{uint256::ONE}; + BOOST_REQUIRE(id.IsValid()); + + CBLSSecretKey sk; + sk.MakeNewKey(); + + const BLSVerificationVectorPtr null_vvec; // default-constructed null shared_ptr + BOOST_REQUIRE(null_vvec == nullptr); + + // Must return false (verification failed) instead of dereferencing the null vvec. + BOOST_CHECK(worker.AsyncVerifyContributionShare(id, null_vvec, sk).get() == false); + + worker.Stop(); +} + +BOOST_AUTO_TEST_CASE(bls_verify_contribution_share_null_vvec_tests) +{ + FuncVerifyContributionShareNullVvec(true); + FuncVerifyContributionShareNullVvec(false); +} + // A dummy BLS object that satisfies the minimal interface expected by CBLSLazyWrapper. class DummyBLS { From 0a681dbbdf94e52f9f54eed6a523fd01fdfb42e1 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 29 Jun 2026 20:46:24 -0500 Subject: [PATCH 3/9] instantsend: cap islock inputs and bound the pending-lock queues Inbound ISDLOCK messages from any peer are deserialized, hashed and retained before BLS signature verification, with three unbounded dimensions an attacker could exploit. Oversized lock: NetInstantSend::ProcessMessage now rejects locks whose input count exceeds InstantSendLock::MAX_INPUTS immediately after deserialization -- before ::SerializeHash() and the O(n) dedup in TriviallyValid() -- so an oversized lock no longer forces a second full serialize+hash before being scored. MAX_INPUTS is derived as MaxBlockSize() / 41 (~48,780): a consensus-valid transaction must fit in a block and each CTxIn is >=41 bytes on the wire, so no valid transaction -- and therefore no valid islock -- can exceed it. Deriving from MaxBlockSize() (now constexpr) keeps the cap correct if the maximum block size ever changes. The cap is also kept in TriviallyValid() as defense-in-depth. Unbounded received queue: EnqueueInstantSendLock() drops new locks once pendingInstantSendLocks reaches MAX_PENDING_INSTANTSEND_LOCKS (1024). Unbounded awaiting-tx queue: pendingNoTxInstantSendLocks holds BLS-verified locks whose transaction has not yet arrived; a peer with quorum-signing power could fill it with locks for transactions that never appear. It is now bounded by the same ceiling. Honest islocks are re-relayed, so dropping under flood is not fatal. --- src/consensus/consensus.h | 2 +- src/instantsend/instantsend.cpp | 17 ++++++++++++++++- src/instantsend/instantsend.h | 8 ++++++++ src/instantsend/lock.cpp | 2 +- src/instantsend/lock.h | 10 ++++++++++ src/instantsend/net_instantsend.cpp | 9 +++++++++ src/test/evo_islock_tests.cpp | 24 ++++++++++++++++++++++++ 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index adc95252cf8b..625d67e4621d 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -9,7 +9,7 @@ /** The maximum allowed size for a serialized block, in bytes (network rule) */ static const unsigned int MAX_LEGACY_BLOCK_SIZE = 1000000; static const unsigned int MAX_DIP0001_BLOCK_SIZE = 2000000; -inline unsigned int MaxBlockSize(bool fDIP0001Active = true) +constexpr unsigned int MaxBlockSize(bool fDIP0001Active = true) { return fDIP0001Active ? MAX_DIP0001_BLOCK_SIZE : MAX_LEGACY_BLOCK_SIZE; } diff --git a/src/instantsend/instantsend.cpp b/src/instantsend/instantsend.cpp index 0726da6911e3..8d39697608bd 100644 --- a/src/instantsend/instantsend.cpp +++ b/src/instantsend/instantsend.cpp @@ -86,6 +86,13 @@ void CInstantSendManager::EnqueueInstantSendLock(NodeId from, const uint256& has } LOCK(cs_pendingLocks); + if (pendingInstantSendLocks.size() >= MAX_PENDING_INSTANTSEND_LOCKS) { + // Drop instead of growing the queue without bound. A peer cannot pin + // unbounded memory with unverified islocks; honest islocks are re-relayed. + LogPrint(BCLog::INSTANTSEND, "CInstantSendManager::%s -- pending islock queue full (%d), dropping islock=%s peer=%d\n", + __func__, pendingInstantSendLocks.size(), hash.ToString(), from); + return; + } pendingInstantSendLocks.emplace(hash, instantsend::PendingISLockFromPeer{from, std::move(islock)}); } @@ -175,7 +182,15 @@ std::variant CInstantSendManager::Proc } else { // put it in a separate pending map and try again later LOCK(cs_pendingLocks); - pendingNoTxInstantSendLocks.try_emplace(hash, instantsend::PendingISLockFromPeer{from, islock}); + if (pendingNoTxInstantSendLocks.size() >= MAX_PENDING_INSTANTSEND_LOCKS) { + // Bound this queue too: a malicious quorum could otherwise mint valid islocks + // for transactions that never arrive, growing it without limit. + // Honest islocks are re-relayed, so dropping under flood is not fatal. + LogPrint(BCLog::INSTANTSEND, "CInstantSendManager::%s -- no-tx pending islock queue full (%d), dropping islock=%s peer=%d\n", + __func__, pendingNoTxInstantSendLocks.size(), hash.ToString(), from); + } else { + pendingNoTxInstantSendLocks.try_emplace(hash, instantsend::PendingISLockFromPeer{from, islock}); + } } // This will also add children TXs to pendingRetryTxs diff --git a/src/instantsend/instantsend.h b/src/instantsend/instantsend.h index dde3739d6030..9b6dea2bb83b 100644 --- a/src/instantsend/instantsend.h +++ b/src/instantsend/instantsend.h @@ -76,6 +76,14 @@ class CInstantSendManager final : public instantsend::InstantSendSignerParent std::atomic m_signer{nullptr}; + // Hard ceiling on the number of peer-supplied InstantSend locks retained in each of + // the pending queues -- pendingInstantSendLocks (received, not yet BLS-verified) and + // pendingNoTxInstantSendLocks (verified, awaiting the locked transaction). The work + // thread drains continuously, so normal operation keeps only a handful pending; this + // never triggers legitimately, but it bounds memory if a peer floods locks faster than + // they can be processed. + static constexpr size_t MAX_PENDING_INSTANTSEND_LOCKS{1024}; + mutable Mutex cs_pendingLocks; // Incoming and not verified yet Uint256HashMap pendingInstantSendLocks GUARDED_BY(cs_pendingLocks); diff --git a/src/instantsend/lock.cpp b/src/instantsend/lock.cpp index f0f2c1d1508d..f768c033708f 100644 --- a/src/instantsend/lock.cpp +++ b/src/instantsend/lock.cpp @@ -29,7 +29,7 @@ uint256 InstantSendLock::GetRequestId() const */ bool InstantSendLock::TriviallyValid() const { - if (txid.IsNull() || inputs.empty()) { + if (txid.IsNull() || inputs.empty() || inputs.size() > MAX_INPUTS) { return false; } diff --git a/src/instantsend/lock.h b/src/instantsend/lock.h index b0536e8421bc..852fc67bebb5 100644 --- a/src/instantsend/lock.h +++ b/src/instantsend/lock.h @@ -6,6 +6,7 @@ #define BITCOIN_INSTANTSEND_LOCK_H #include +#include #include #include @@ -18,6 +19,15 @@ class COutPoint; namespace instantsend { struct InstantSendLock { static constexpr uint8_t CURRENT_VERSION{1}; + // An islock pins the same outpoints as the locked transaction's inputs, so it can + // never carry more inputs than a consensus-valid transaction. Such a transaction must + // fit in a block (MaxBlockSize()) and each transaction input (CTxIn) serializes to at + // least 41 bytes (COutPoint 36 + scriptSig length 1 + nSequence 4), bounding it at + // MaxBlockSize() / 41 inputs; the islock stores those inputs as 36-byte COutPoints. + // Deriving from MaxBlockSize() keeps this cap in lockstep with any future block-size + // change. The ceiling can never reject a valid islock, but it lets us drop oversized + // locks before the O(n) hashing/dedup work and bounds each retained pending entry. + static constexpr size_t MAX_INPUTS{MaxBlockSize() / 41}; uint8_t nVersion{CURRENT_VERSION}; std::vector inputs; diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index 64fcfd0fd8ee..45821225f724 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -212,6 +212,15 @@ void NetInstantSend::ProcessMessage(CNode& pfrom, const std::string& msg_type, C vRecv >> *islock; const NodeId from = pfrom.GetId(); + + // Reject oversized locks before any O(n) work (hashing, dedup). A consensus-valid + // transaction -- and therefore a valid islock -- can never exceed MAX_INPUTS inputs, + // so this cannot drop a legitimate lock. + if (islock->inputs.size() > instantsend::InstantSendLock::MAX_INPUTS) { + m_peer_manager->PeerMisbehaving(from, INVALID_ISLOCK_MISBEHAVIOR_SCORE); + return; + } + uint256 hash = ::SerializeHash(*islock); WITH_LOCK(::cs_main, m_peer_manager->PeerEraseObjectRequest(from, CInv{MSG_ISDLOCK, hash})); diff --git a/src/test/evo_islock_tests.cpp b/src/test/evo_islock_tests.cpp index 4d40b139af58..2e681b906236 100644 --- a/src/test/evo_islock_tests.cpp +++ b/src/test/evo_islock_tests.cpp @@ -208,4 +208,28 @@ BOOST_AUTO_TEST_CASE(geninputlockrequestid_edge_cases) BOOST_CHECK(nullRequestId != maxIndexRequestId); } +// Regression test for the islock input cap: an oversized input vector must be +// rejected by TriviallyValid(), while the cap is derived so it can never reject a valid lock. +BOOST_AUTO_TEST_CASE(trivially_valid_input_cap) +{ + // A lock with a non-null txid and a few unique inputs is trivially valid. + instantsend::InstantSendLock islock; + islock.txid = uint256::ONE; + islock.inputs = {COutPoint(uint256::ONE, 0), COutPoint(uint256::ONE, 1)}; + BOOST_CHECK(islock.TriviallyValid()); + + // MAX_INPUTS is derived from MaxBlockSize(): a consensus-valid transaction must fit in a + // block and each input is >=41 bytes on the wire, so it can never carry more than + // MaxBlockSize() / 41 inputs. The cap thus cannot reject a legitimate islock, and it + // tracks any future block-size change. + BOOST_CHECK_EQUAL(instantsend::InstantSendLock::MAX_INPUTS, MaxBlockSize() / 41); + + // A lock carrying more than MAX_INPUTS inputs is rejected up front, before any O(n) + // hashing/dedup work, so a peer cannot pin an oversized input vector. + instantsend::InstantSendLock oversized; + oversized.txid = uint256::ONE; + oversized.inputs.assign(instantsend::InstantSendLock::MAX_INPUTS + 1, COutPoint(uint256::ONE, 0)); + BOOST_CHECK(!oversized.TriviallyValid()); +} + BOOST_AUTO_TEST_SUITE_END() From b6e881ad17ca595e310b87853facad9f74b76b77 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 29 Jun 2026 20:46:24 -0500 Subject: [PATCH 4/9] governance: bound govsync bloom filter hash-function count The per-object MNGOVERNANCESYNC vote-sync path passes a peer-supplied CBloomFilter to GetSyncableVoteInvs(), which calls filter.contains() on every cached vote; contains() loops nHashFuncs times. nHashFuncs is deserialized without bounds, so a ~43-byte request with nHashFuncs=0xFFFFFFFF forces ~4.3 billion MurmurHash3 evaluations per cached vote while g_msgproc_mutex is held, stalling all P2P message processing. Reject filters that violate CBloomFilter::IsWithinSizeConstraints() (vData <= 36000 bytes, nHashFuncs <= 50) right after deserialization, with Misbehaving(100), mirroring the existing filterload handler. Legitimate filters are constructed within these bounds, so this only rejects abusive requests. --- src/governance/net_governance.cpp | 9 ++++ test/functional/p2p_govsync_bloom.py | 70 ++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 3 files changed, 80 insertions(+) create mode 100755 test/functional/p2p_govsync_bloom.py diff --git a/src/governance/net_governance.cpp b/src/governance/net_governance.cpp index 7849ff91544e..bb572a1f7fec 100644 --- a/src/governance/net_governance.cpp +++ b/src/governance/net_governance.cpp @@ -75,6 +75,15 @@ void NetGovernance::ProcessMessage(CNode& peer, const std::string& msg_type, CDa vRecv >> nProp; vRecv >> filter; + // The per-object vote-sync path tests this peer-supplied filter against every + // cached vote (CBloomFilter::contains() loops nHashFuncs times). An unbounded + // nHashFuncs would force billions of MurmurHash3 evaluations per vote while the + // message-processing mutex is held. Enforce the same bound filterload uses. + if (!filter.IsWithinSizeConstraints()) { + m_peer_manager->PeerMisbehaving(peer.GetId(), 100); + return; + } + LogPrint(BCLog::GOBJECT, "MNGOVERNANCESYNC -- syncing governance objects to our peer %s\n", peer.GetLogString()); if (nProp == uint256()) { // Full sync of all governance objects diff --git a/test/functional/p2p_govsync_bloom.py b/test/functional/p2p_govsync_bloom.py new file mode 100755 index 000000000000..9ade5f5ba64a --- /dev/null +++ b/test/functional/p2p_govsync_bloom.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# 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. +"""Test that an oversized govsync bloom filter is rejected. + +A MNGOVERNANCESYNC ("govsync") request carries a peer-supplied CBloomFilter. For a +per-object request the node tests that filter against every cached vote, and +CBloomFilter::contains() loops nHashFuncs times. nHashFuncs is deserialized without +bounds, so an unbounded value would force an enormous amount of work while the message +processing mutex is held. The handler must reject any filter that is not within the +standard size constraints (vData <= 36000 bytes, nHashFuncs <= 50), matching filterload. +""" +import struct + +from test_framework.messages import ser_string, ser_uint256 +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import force_finish_mnsync + +# CBloomFilter size constraints (src/common/bloom.h). +MAX_HASH_FUNCS = 50 + + +class msg_govsync: + """MNGOVERNANCESYNC: a governance-object hash followed by a CBloomFilter.""" + msgtype = b"govsync" + + def __init__(self, nprop=0, data=b"", n_hash_funcs=0, n_tweak=0, n_flags=0): + self.nprop = nprop + self.data = data + self.n_hash_funcs = n_hash_funcs + self.n_tweak = n_tweak + self.n_flags = n_flags + + def serialize(self): + r = ser_uint256(self.nprop) + r += ser_string(self.data) # CBloomFilter.vData + r += struct.pack(" 50) is rejected and the peer disconnected") + bad_peer = node.add_p2p_connection(P2PInterface()) + bad_peer.send_message(msg_govsync(n_hash_funcs=0xFFFFFFFF)) + bad_peer.wait_for_disconnect() + + +if __name__ == '__main__': + GovsyncBloomCapTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index ecb2a49a10eb..536ff5b6351a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -339,6 +339,7 @@ 'feature_new_quorum_type_activation.py', 'feature_governance_objects.py', 'p2p_governance_invs.py', + 'p2p_govsync_bloom.py', 'rpc_uptime.py', 'feature_discover.py', 'wallet_resendwallettransactions.py --legacy-wallet', From cb6afcbe5feaa13213340fbcbcc34f246f2f840f Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 29 Jun 2026 16:53:38 -0500 Subject: [PATCH 5/9] net: avoid ThreadSocketHandler busy-loop on receive-paused peers ThreadSocketHandler uses a Dash-specific edge-triggered scheme: when mapReceivableNodes is non-empty it polls with a zero timeout (the only_poll fast path) instead of waiting SELECT_TIMEOUT_MILLISECONDS, so sockets that still have buffered data keep getting drained. A node enters mapReceivableNodes when its socket signals readable (fHasRecvData=true) and only leaves once SocketRecvData() drains it and clears fHasRecvData. A receive-paused peer (fPauseRecv, set once its queued message bytes exceed -maxreceivebuffer, ~5 MB by default) is skipped by both GenerateWaitSockets() and SocketHandlerConnected() and is therefore never drained, so fHasRecvData stays set and it lingers in mapReceivableNodes. only_poll then stays true and ThreadSocketHandler spins at 100% CPU for the entire pause window. A remote, unauthenticated peer can trigger and sustain this by sending data faster than the node processes it (resending periodically to stay paused and to reset the 20-minute inactivity timer), pinning the single socket-handler thread and degrading network I/O for all peers. Pull the receivable-node check into HasUnpausedReceivableNode() and count a receivable node as work only when it is not paused; while only paused nodes remain receivable, fall back to the normal select/poll wait. The paused peer is still drained promptly once the message handler pops enough messages to clear fPauseRecv. This finishes the partial backport of bitcoin#27981 (commit 8c986d6b08f), which was noted to need revisiting alongside bitcoin#24356. Add net_tests/socket_handler_skips_receive_paused_nodes covering the predicate directly: a lone receive-paused node must not count as work (the regression), while genuinely drainable nodes still do. The busy-loop only manifests under sustained receive-pause, which requires the message-handler thread to fall behind the receive path; that is not reliably reproducible from a functional test without expensive or ban-triggering traffic, so the regression is guarded at the unit level. --- src/net.cpp | 16 +++++++++++++++- src/net.h | 11 +++++++++++ src/test/net_tests.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/net.cpp b/src/net.cpp index 3b70ee45fe00..f186489c743a 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2379,6 +2379,16 @@ Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) return events_per_sock; } +bool HasUnpausedReceivableNode(const std::unordered_map& receivable_nodes) +{ + for (const auto& entry : receivable_nodes) { + if (!entry.second->fPauseRecv) { + return true; + } + } + return false; +} + void CConnman::SocketHandler(CMasternodeSync& mn_sync) { AssertLockNotHeld(m_total_bytes_sent_mutex); @@ -2389,7 +2399,11 @@ void CConnman::SocketHandler(CMasternodeSync& mn_sync) // Check if we have work to do and thus should avoid waiting for events READ_LOCK(m_nodes_mutex); // We acquire this to avoid the pointers stored in mapSendableNodes and mapReceivableNodes being invalidated by ThreadSocketHandler LOCK(cs_sendable_receivable_nodes); - if (!mapReceivableNodes.empty()) { + // A receive-paused node lingers here with its readable flag set but is never drained, so + // it must not count as work; otherwise this polls with a zero timeout every iteration and + // ThreadSocketHandler busy-loops at 100% CPU for the whole (remotely triggerable) pause + // window. See HasUnpausedReceivableNode(). + if (HasUnpausedReceivableNode(mapReceivableNodes)) { return true; } for (const auto& p : mapSendableNodes) { diff --git a/src/net.h b/src/net.h index 8bdda43e9e75..fa8fab9306bc 100644 --- a/src/net.h +++ b/src/net.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -1178,6 +1179,16 @@ class NetEventsInterface ~NetEventsInterface() = default; }; +/** + * Returns true if any node in `receivable_nodes` can currently be drained: it has buffered data + * to read (so it is in the receivable set) and is not receive-paused. A receive-paused node + * lingers in the set with its readable flag set but is skipped by the receive path, so it must + * NOT be treated as actionable work -- otherwise CConnman::SocketHandler() polls with a zero + * timeout on every iteration and ThreadSocketHandler busy-loops at 100% CPU for the whole, + * remotely triggerable, pause window. + */ +bool HasUnpausedReceivableNode(const std::unordered_map& receivable_nodes); + class CConnman { friend class CNode; diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 3c77fed9bd8f..5c0823f835f5 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -1664,4 +1664,41 @@ BOOST_AUTO_TEST_CASE(v2_short_id_version_negotiation) } } +//! Regression test for the ThreadSocketHandler busy-loop on receive-paused peers. +//! +//! CConnman::SocketHandler() polls sockets with a zero timeout (its "only_poll" fast path) +//! whenever a node has actionable receive work, so that sockets with buffered data keep getting +//! drained. A receive-paused peer (fPauseRecv) is skipped by the receive path yet lingers in the +//! receivable set with its readable flag set, so it must not be counted as actionable work -- +//! otherwise the loop would spin at 100% CPU for the whole pause window. HasUnpausedReceivableNode() +//! encodes that rule; verify it ignores paused nodes but still reports genuinely drainable ones. +BOOST_AUTO_TEST_CASE(socket_handler_skips_receive_paused_nodes) +{ + auto make_node = [](NodeId id) { + return std::make_unique( + id, /*sock=*/nullptr, + CAddress{CService{}, NODE_NONE}, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, + CAddress{}, /*addrNameIn=*/std::string{}, + ConnectionType::INBOUND, /*inbound_onion=*/false); + }; + auto paused = make_node(1); + auto active = make_node(2); + paused->fPauseRecv = true; + active->fPauseRecv = false; + + std::unordered_map receivable; + // No receivable nodes -> nothing to drain. + BOOST_CHECK(!HasUnpausedReceivableNode(receivable)); + // A lone receive-paused node must NOT count as work (this is the bug being guarded against). + receivable.emplace(paused->GetId(), paused.get()); + BOOST_CHECK(!HasUnpausedReceivableNode(receivable)); + // A node that can actually be drained does count as work. + receivable.emplace(active->GetId(), active.get()); + BOOST_CHECK(HasUnpausedReceivableNode(receivable)); + // Once the paused node is unpaused (drained by the message handler), it counts again. + receivable.erase(active->GetId()); + paused->fPauseRecv = false; + BOOST_CHECK(HasUnpausedReceivableNode(receivable)); +} + BOOST_AUTO_TEST_SUITE_END() From 1cdc55a958d8e8b0c5ca06fbab6a4e5fb15b5ca6 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 30 Jun 2026 08:23:13 -0500 Subject: [PATCH 6/9] Merge #7392: backport: bitcoin#30633 c93513b46a3c530c165fc33f89ccd1edfefeebec Merge bitcoin/bitcoin#30633: Fixes for GCC 15 compatibility (merge-script) Pull request description: ## Issue being fixed or feature implemented Dash Core is failed to get compiled with warnings enabled on gcc-16 ## What was done? Backport bitcoin#30633 ## How Has This Been Tested? Build passes _these_ relevant errors ## Breaking Changes N/A ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone ACKs for top commit: UdjinM6: utACK c93513b46a3c530c165fc33f89ccd1edfefeebec Tree-SHA512: c9b5537ff911b7fbc3eacd0517e7c67ccac907ce2d6983a1671c3a28196e460234d87e4b947536b71b310b44a1cbeb0341c3dc36c3bc6b98075c41de58b92238 --- src/chainparamsbase.h | 1 + src/evo/chainhelper.h | 1 + src/node/interface_ui.h | 3 ++- src/policy/feerate.h | 4 +--- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/chainparamsbase.h b/src/chainparamsbase.h index 718392e04def..c8f94901b495 100644 --- a/src/chainparamsbase.h +++ b/src/chainparamsbase.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_CHAINPARAMSBASE_H #define BITCOIN_CHAINPARAMSBASE_H +#include #include #include diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index 69a229d93cad..c39dc9e88ac1 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_EVO_CHAINHELPER_H #define BITCOIN_EVO_CHAINHELPER_H +#include #include #include #include diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 67a6d0f5b1b0..c4b36ef07cfb 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -6,9 +6,10 @@ #ifndef BITCOIN_NODE_INTERFACE_UI_H #define BITCOIN_NODE_INTERFACE_UI_H +#include #include -#include #include +#include class CBlockIndex; enum class SynchronizationState; diff --git a/src/policy/feerate.h b/src/policy/feerate.h index 7b7e6c5d38dd..15bdd970c3df 100644 --- a/src/policy/feerate.h +++ b/src/policy/feerate.h @@ -38,10 +38,8 @@ class CFeeRate public: /** Fee rate of 0 satoshis per kvB */ CFeeRate() : nSatoshisPerK(0) { } - template + template // Disallow silent float -> int conversion explicit CFeeRate(const I _nSatoshisPerK): nSatoshisPerK(_nSatoshisPerK) { - // We've previously had bugs creep in from silent double->int conversion... - static_assert(std::is_integral::value, "CFeeRate should be used without floats"); } /** From c4eafd2e14656fca22c299b414073750688c6682 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 30 Jun 2026 08:24:13 -0500 Subject: [PATCH 7/9] Merge #7393: fix: compilation error with modern gcc for leveldb subtree 75b19b3cbf573484ec3aa30404e0492a32969cec fix: compilation error with modern gcc for leveldb subtree (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented Dash Core's build fails with gcc-16: In copy constructor 'constexpr leveldb::DBImpl::CompactionState::Output::Output(const leveldb::DBImpl::CompactionState::Output&)', inlined from 'constexpr _Tp* std::construct_at(_Tp*, _Args&& ...) [with _Tp = leveldb::DBImpl::CompactionState::Output; _Args = {const leveldb::DBImpl::CompactionState::Output&}]' at /usr/include/c++/16/bits/stl_construct.h:110:9, inlined from 'static constexpr void std::allocator_traits >::construct(allocator_type&, _Up*, _Args&& ...) [with _Up = leveldb::DBImpl::CompactionState::Output; _Args = {const leveldb::DBImpl::CompactionState::Output&}; _Tp = leveldb::DBImpl::CompactionState::Output]' at /usr/include/c++/16/bits/alloc_traits.h:716:21, inlined from 'constexpr void std::vector<_Tp, _Alloc>::_M_realloc_append(_Args&& ...) [with _Args = {const leveldb::DBImpl::CompactionState::Output&}; _Tp = leveldb::DBImpl::CompactionState::Output; _Alloc = std::allocator]' at /usr/include/c++/16/bits/vector.tcc:594:26, inlined from 'constexpr void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = leveldb::DBImpl::CompactionState::Output; _Alloc = std::allocator]' at /usr/include/c++/16/bits/stl_vector.h:1417:21, inlined from 'leveldb::Status leveldb::DBImpl::OpenCompactionOutputFile(CompactionState*)' at leveldb/db/db_impl.cc:808:31: leveldb/db/db_impl.cc:57:10: error: 'out.leveldb::DBImpl::CompactionState::Output::file_size' may be used uninitialized [-Werror=maybe-uninitialized] 57 | struct Output { | ^~~~~~ leveldb/db/db_impl.cc: In member function 'leveldb::Status leveldb::DBImpl::OpenCompactionOutputFile(CompactionState*)': leveldb/db/db_impl.cc:804:29: note: 'out' declared here 804 | CompactionState::Output out; ## What was done? Original leveldb's commit is ad9b1c989380538bfb19c5c65df0d3f72d8ed62b Backported to Bitcoin Core by bitcoin/bitcoin#34470 Though, that too many unrelated changes meanwhile. That's temporary fix until leveldb's version won't be properly updated. ## How Has This Been Tested? Together with https://github.com/dashpay/dash/pull/7392 I finally compile dash core with gcc 16 again. ## Breaking Changes N/A ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: UdjinM6: utACK 75b19b3cbf573484ec3aa30404e0492a32969cec Tree-SHA512: 985042f04a4df7b20aaa773d302401af948099ae8c29d6669f0441e654b9c8847942e75574c52d1652411e321b80b1861f93c163792636d1246bea9edc99c249 --- src/leveldb/db/db_impl.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/leveldb/db/db_impl.cc b/src/leveldb/db/db_impl.cc index 65e31724bcec..10f523257c1b 100644 --- a/src/leveldb/db/db_impl.cc +++ b/src/leveldb/db/db_impl.cc @@ -803,6 +803,7 @@ Status DBImpl::OpenCompactionOutputFile(CompactionState* compact) { pending_outputs_.insert(file_number); CompactionState::Output out; out.number = file_number; + out.file_size = 0; out.smallest.Clear(); out.largest.Clear(); compact->outputs.push_back(out); From 01fc719f9088c7ec3d14cfb04a1a8bd296537934 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 30 Jun 2026 08:37:42 -0500 Subject: [PATCH 8/9] chore: prepare v23.1.7 release --- configure.ac | 2 +- .../flatpak/org.dash.dash-core.metainfo.xml | 1 + doc/man/dash-cli.1 | 6 +- doc/man/dash-qt.1 | 12 ++-- doc/man/dash-tx.1 | 6 +- doc/man/dash-util.1 | 6 +- doc/man/dash-wallet.1 | 6 +- doc/man/dashd.1 | 10 +-- doc/release-notes.md | 42 ++++++++--- .../dash/release-notes-23.1.5.md | 70 +++++++++++++++++++ 10 files changed, 127 insertions(+), 34 deletions(-) create mode 100644 doc/release-notes/dash/release-notes-23.1.5.md diff --git a/configure.ac b/configure.ac index ea46c13b1265..91beef9b689a 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, 5) +define(_CLIENT_VERSION_BUILD, 7) define(_CLIENT_VERSION_IS_RELEASE, true) 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 486118c24fd2..30227f6b4771 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 751623230e27..2d5739f408f7 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.5" "User Commands" +.TH DASH-CLI "1" "June 2026" "dash-cli v23.1.7" "User Commands" .SH NAME -dash-cli \- manual page for dash-cli v23.1.5 +dash-cli \- manual page for dash-cli v23.1.7 .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.5 .B dash-cli [\fI\,options\/\fR] \fI\,help Get help for a command\/\fR .SH DESCRIPTION -Dash Core RPC client version v23.1.5 +Dash Core RPC client version v23.1.7 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-qt.1 b/doc/man/dash-qt.1 index 09fb78c87c43..d906b5960f6c 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.5" "User Commands" +.TH DASH-QT "1" "June 2026" "dash-qt v23.1.7" "User Commands" .SH NAME -dash-qt \- manual page for dash-qt v23.1.5 +dash-qt \- manual page for dash-qt v23.1.7 .SH SYNOPSIS .B dash-qt [\fI\,command-line options\/\fR] [\fI\,URI\/\fR] .SH DESCRIPTION -Dash Core version v23.1.5 +Dash Core version v23.1.7 .PP Optional URI is a Dash address in BIP21 URI format. .SH OPTIONS @@ -128,12 +128,12 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-12\fR to 15, 0 = auto, <0 = +Set the number of script verification threads (\fB\-14\fR to 15, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-parbls=\fR .IP -Set the number of BLS verification threads (\fB\-12\fR to 33, 0 = auto, <0 = +Set the number of BLS verification threads (\fB\-14\fR to 33, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-persistmempool\fR @@ -1172,7 +1172,7 @@ Set the font weight for bold texts. Possible range 0 to 8 (default: 4) .HP \fB\-font\-weight\-normal\fR .IP -Set the font weight for normal texts. Possible range 0 to 8 (default: 2) +Set the font weight for normal texts. Possible range 0 to 8 (default: 1) .HP \fB\-lang=\fR .IP diff --git a/doc/man/dash-tx.1 b/doc/man/dash-tx.1 index 9e7b735370ad..fa7fc530064b 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.5" "User Commands" +.TH DASH-TX "1" "June 2026" "dash-tx v23.1.7" "User Commands" .SH NAME -dash-tx \- manual page for dash-tx v23.1.5 +dash-tx \- manual page for dash-tx v23.1.7 .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.5 .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.5 +Dash Core dash\-tx utility version v23.1.7 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-util.1 b/doc/man/dash-util.1 index b65ba3b541bc..f1a1e4ccf654 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.5" "User Commands" +.TH DASH-UTIL "1" "June 2026" "dash-util v23.1.7" "User Commands" .SH NAME -dash-util \- manual page for dash-util v23.1.5 +dash-util \- manual page for dash-util v23.1.7 .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.5 +Dash Core dash\-util utility version v23.1.7 .SH OPTIONS .HP \-? diff --git a/doc/man/dash-wallet.1 b/doc/man/dash-wallet.1 index 73aa81e1701d..b942f301f180 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.5" "User Commands" +.TH DASH-WALLET "1" "June 2026" "dash-wallet v23.1.7" "User Commands" .SH NAME -dash-wallet \- manual page for dash-wallet v23.1.5 +dash-wallet \- manual page for dash-wallet v23.1.7 .SH DESCRIPTION -Dash Core dash\-wallet version v23.1.5 +Dash Core dash\-wallet version v23.1.7 .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 6a42edae0f78..011b448337c3 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.5" "User Commands" +.TH DASHD "1" "June 2026" "dashd v23.1.7" "User Commands" .SH NAME -dashd \- manual page for dashd v23.1.5 +dashd \- manual page for dashd v23.1.7 .SH SYNOPSIS .B dashd [\fI\,options\/\fR] \fI\,Start Dash Core\/\fR .SH DESCRIPTION -Dash Core version v23.1.5 +Dash Core version v23.1.7 .SH OPTIONS .HP \-? @@ -126,12 +126,12 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-12\fR to 15, 0 = auto, <0 = +Set the number of script verification threads (\fB\-14\fR to 15, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-parbls=\fR .IP -Set the number of BLS verification threads (\fB\-12\fR to 33, 0 = auto, <0 = +Set the number of BLS verification threads (\fB\-14\fR to 33, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-persistmempool\fR diff --git a/doc/release-notes.md b/doc/release-notes.md index aeda56d9d27d..5e4e5e7628b3 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,7 +1,8 @@ -# Dash Core version v23.1.5 +# Dash Core version v23.1.7 -This is a new patch version release, bringing bug fixes. -This release is **optional** for all nodes, although recommended. +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: @@ -25,15 +26,34 @@ require a reindex. # Release Notes -## Bug Fixes +## Security -- Corrected the checkpoint hash for height 2487500 (dash#7368). +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. -## Documentation +- 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. -- Updated the v23.1.4 release notes intro and wording (dash#7369). +## Build -# v23.1.5 Change log +- 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]. @@ -41,7 +61,8 @@ See detailed [set of changes][set-of-changes]. Thanks to everyone who directly contributed to this release: -- PastaClaw +- knst +- PastaPastaPasta As well as everyone that submitted issues, reviewed pull requests and helped debug the release candidates. @@ -50,6 +71,7 @@ debug the release candidates. 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 @@ -67,4 +89,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/9adc0b16f93d15fe065692cbe77f3950419db0cb...dashpay:v23.1.5 +[set-of-changes]: https://github.com/dashpay/dash/compare/v23.1.5...dashpay:v23.1.7 diff --git a/doc/release-notes/dash/release-notes-23.1.5.md b/doc/release-notes/dash/release-notes-23.1.5.md new file mode 100644 index 000000000000..aeda56d9d27d --- /dev/null +++ b/doc/release-notes/dash/release-notes-23.1.5.md @@ -0,0 +1,70 @@ +# Dash Core version v23.1.5 + +This is a new patch version release, bringing bug fixes. +This release is **optional** for all nodes, although recommended. + +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 + +## Bug Fixes + +- Corrected the checkpoint hash for height 2487500 (dash#7368). + +## Documentation + +- Updated the v23.1.4 release notes intro and wording (dash#7369). + +# v23.1.5 Change log + +See detailed [set of changes][set-of-changes]. + +# Credits + +Thanks to everyone who directly contributed to this release: + +- PastaClaw + +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.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/9adc0b16f93d15fe065692cbe77f3950419db0cb...dashpay:v23.1.5 From 2dd61907eab50330f8478cc03c28962f2d596be7 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 1 Jul 2026 12:24:15 -0500 Subject: [PATCH 9/9] fix: keep DKG session header self-contained --- src/llmq/dkgsession.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/llmq/dkgsession.h b/src/llmq/dkgsession.h index f959367a8681..91b77abf1cf2 100644 --- a/src/llmq/dkgsession.h +++ b/src/llmq/dkgsession.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include