diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index 07b89e270203..e79c5fa4248e 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -57,9 +57,7 @@ jobs: sparse-checkout: | ci/dash ci/test - depends/Makefile - depends/packages - depends/hosts + depends contrib/containers/ci/ci.Dockerfile contrib/containers/ci/ci-slim.Dockerfile contrib/containers/guix/scripts/setup-sdk @@ -75,9 +73,9 @@ jobs: DEP_HASH="$(echo -n "${BUILD_TARGET}" "${DEP_OPTS}" "${HOST}" | sha256sum | head -c 64)" echo "DEP_HASH=${DEP_HASH}" >> "${GITHUB_OUTPUT}" DOCKERFILE_HASH="${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'contrib/containers/ci/ci-slim.Dockerfile') }}" - PACKAGES_HASH="${{ hashFiles('depends/packages/*', 'depends/Makefile') }}" + DEPENDS_INPUTS_HASH="${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/gen_id', 'depends/config.guess', 'depends/config.sub', 'depends/config.site.in', 'depends/builders/**', 'depends/hosts/**', 'depends/packages/**', 'depends/patches/**') }}" CACHE_KEY_PREFIX="depends-${DOCKERFILE_HASH}-${{ inputs.base-image-digest }}-${{ inputs.runs-on }}-${{ inputs.build-target }}" - CACHE_KEY="${CACHE_KEY_PREFIX}-${DEP_HASH}-${PACKAGES_HASH}" + CACHE_KEY="${CACHE_KEY_PREFIX}-${DEP_HASH}-${DEPENDS_INPUTS_HASH}" echo "cache-key-prefix=${CACHE_KEY_PREFIX}" >> "${GITHUB_OUTPUT}" echo "cache-key=${CACHE_KEY}" >> "${GITHUB_OUTPUT}" echo "Cache key: ${CACHE_KEY}" diff --git a/AGENTS.md b/AGENTS.md index d016a2432721..6e5a58e76777 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,12 @@ CI/lint list changes. ## Test Commands Choose tests based on the files touched. Do not claim broad validation if only a -targeted test was run. +targeted test was run. Prefer adding test cases to existing files over creating a +new unit or functional test file. Only create a new test file when the additions +would make an existing file overly complicated, when a separate file yields +clearly improved performance (e.g. parallel execution or isolation), or when the +subject being tested is distinctly separate and does not logically belong in an +existing file. Fewer files reduce test setup overhead and compilation time. ```bash # All unit tests @@ -176,16 +181,12 @@ For these areas, prefer small tests that prove the invariant being changed. ## PR Hygiene +- When creating pull requests, follow `.github/PULL_REQUEST_TEMPLATE.md` for the description and ensure the PR title satisfies the active linter in `.github/workflows/semantic-pull-request.yml` (using Conventional Commits, including `backport:` for Bitcoin Core backports). - Use atomic commits. Each commit should make sense on its own and generally build and pass tests. An intentionally non-building commit (e.g. a regression test landing before its fix) is fine if called out explicitly so it isn't mistaken for an oversight. -- PR titles follow Conventional Commits, including `backport:` for Bitcoin Core - backports. Valid types/scopes are enforced by CI; see - `.github/workflows/semantic-pull-request.yml`. -- PR descriptions must follow `.github/PULL_REQUEST_TEMPLATE.md`: remove the - italicized helper prompts, fill in the required sections, and keep the - checklist accurate for the change. +- Remove the italicized helper prompts from `.github/PULL_REQUEST_TEMPLATE.md`, fill in the required sections, and keep the checklist accurate for the change. - Do not put `@` mentions in PR descriptions; they are copied into merge commits and notify users repeatedly. - Explain what changed and why. For bug fixes, include the failure mode and why diff --git a/CLAUDE.md b/CLAUDE.md index d016a2432721..6e5a58e76777 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,7 +98,12 @@ CI/lint list changes. ## Test Commands Choose tests based on the files touched. Do not claim broad validation if only a -targeted test was run. +targeted test was run. Prefer adding test cases to existing files over creating a +new unit or functional test file. Only create a new test file when the additions +would make an existing file overly complicated, when a separate file yields +clearly improved performance (e.g. parallel execution or isolation), or when the +subject being tested is distinctly separate and does not logically belong in an +existing file. Fewer files reduce test setup overhead and compilation time. ```bash # All unit tests @@ -176,16 +181,12 @@ For these areas, prefer small tests that prove the invariant being changed. ## PR Hygiene +- When creating pull requests, follow `.github/PULL_REQUEST_TEMPLATE.md` for the description and ensure the PR title satisfies the active linter in `.github/workflows/semantic-pull-request.yml` (using Conventional Commits, including `backport:` for Bitcoin Core backports). - Use atomic commits. Each commit should make sense on its own and generally build and pass tests. An intentionally non-building commit (e.g. a regression test landing before its fix) is fine if called out explicitly so it isn't mistaken for an oversight. -- PR titles follow Conventional Commits, including `backport:` for Bitcoin Core - backports. Valid types/scopes are enforced by CI; see - `.github/workflows/semantic-pull-request.yml`. -- PR descriptions must follow `.github/PULL_REQUEST_TEMPLATE.md`: remove the - italicized helper prompts, fill in the required sections, and keep the - checklist accurate for the change. +- Remove the italicized helper prompts from `.github/PULL_REQUEST_TEMPLATE.md`, fill in the required sections, and keep the checklist accurate for the change. - Do not put `@` mentions in PR descriptions; they are copied into merge commits and notify users repeatedly. - Explain what changed and why. For bug fixes, include the failure mode and why diff --git a/ci/test/00_setup_env_native_tsan.sh b/ci/test/00_setup_env_native_tsan.sh index f84f594735f6..9a7f15cf8b44 100755 --- a/ci/test/00_setup_env_native_tsan.sh +++ b/ci/test/00_setup_env_native_tsan.sh @@ -30,6 +30,6 @@ export DEP_OPTS="CC=clang-19 CXX='clang++-19 -stdlib=libc++'" export TEST_RUNNER_EXTRA="--extended --exclude feature_pruning,feature_dbcrash,wallet_multiwallet.py" # Temporarily suppress ASan heap-use-after-free (see issue #14163) export TEST_RUNNER_EXTRA="${TEST_RUNNER_EXTRA} --timeout-factor=4" # Increase timeout because sanitizers slow down export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --with-sanitizers=thread CC=clang-19 CXX=clang++-19 CXXFLAGS='-g'" +export BITCOIN_CONFIG="--enable-zmq --with-sanitizers=thread CC=clang-19 CXX=clang++-19" export CPPFLAGS="-DARENA_DEBUG -DDEBUG_LOCKORDER -DDEBUG_LOCKCONTENTION" export PYZMQ=true diff --git a/configure.ac b/configure.ac index 419f2b537732..1fcbafbd99fd 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]) @@ -1228,16 +1228,10 @@ if test "$enable_stacktraces" != "no"; then ]) fi BACKTRACE_LIBS="$BACKTRACE_LIBS -lbacktrace" - dnl More modern compilers may emit DWARF 5 binaries by default, use DWARF 4 out of precaution - if test "$TARGET_OS" != "windows"; then - AX_CHECK_COMPILE_FLAG([-gdwarf-4], [BACKTRACE_FLAGS="$BACKTRACE_FLAGS -gdwarf-4"], [ - if test "$enable_stacktraces" = "yes"; then - AC_MSG_ERROR([--enable-stacktraces was specified but cannot set -gdwarf-4]) - fi - AC_MSG_WARN([cannot set -gdwarf-4, stacktraces will be disabled]) - enable_stacktraces=no - ], [$CXXFLAG_WERROR]) - fi + dnl The libbacktrace pinned in depends understands DWARF 5, so the + dnl compiler's default debug format is used as-is. A -gdwarf-4 pin + dnl previously lived here and, being a -g-family flag, also forced + dnl debug level 2 onto every build. if test "$TARGET_OS" = "darwin"; then AX_CHECK_COMPILE_FLAG([-fno-standalone-debug], [BACKTRACE_FLAGS="$BACKTRACE_FLAGS -fno-standalone-debug"], [ if test "$enable_stacktraces" = "yes"; then @@ -2139,10 +2133,10 @@ echo " target os = $host_os" echo " build os = $build_os" echo echo " CC = $CC" -echo " CFLAGS = $DEBUG_CFLAGS $PTHREAD_CFLAGS $BACKTRACE_FLAGS $CFLAGS" +echo " CFLAGS = $BACKTRACE_FLAGS $DEBUG_CFLAGS $PTHREAD_CFLAGS $CFLAGS" echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CORE_CPPFLAGS $CPPFLAGS" echo " CXX = $CXX" -echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $CORE_CXXFLAGS $BACKTRACE_FLAGS $CXXFLAGS" +echo " CXXFLAGS = $BACKTRACE_FLAGS $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $CORE_CXXFLAGS $CXXFLAGS" echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $CORE_LDFLAGS $BACKTRACE_LDFLAGS $LDFLAGS" echo " AR = $AR" echo " ARFLAGS = $ARFLAGS" 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/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh index 8c517368d17a..0c9fda7ad11d 100755 --- a/contrib/guix/libexec/build.sh +++ b/contrib/guix/libexec/build.sh @@ -222,7 +222,13 @@ HOST_CFLAGS+=$(find /gnu/store -maxdepth 1 -mindepth 1 -type d -exec echo -n " - case "$HOST" in *linux*) HOST_CFLAGS+=" -ffile-prefix-map=${PWD}=." ;; *mingw*) HOST_CFLAGS+=" -fno-ident" ;; - *darwin*) unset HOST_CFLAGS ;; + *darwin*) + # Optimization and target flags come from depends' config.site; only + # the debug level is requested here. The shipped dSYM debug artifacts + # previously carried full -g2 DWARF via the since-removed -gdwarf-4 + # backtrace flag escalating configure's -g1 floor, so keep requesting + # it explicitly. + HOST_CFLAGS="-g" ;; esac # CXXFLAGS diff --git a/depends/hosts/darwin.mk b/depends/hosts/darwin.mk index 977931c14855..971347de5ae9 100644 --- a/depends/hosts/darwin.mk +++ b/depends/hosts/darwin.mk @@ -57,15 +57,25 @@ darwin_STRIP=$(shell $(SHELL) $(.SHELLFLAGS) "command -v llvm-strip") # in the SDK, where __has_feature(modules) is used to define USE_CLANG_TYPES, # which is in turn used as an include guard. -# TODO: remove C_INCLUDE_PATH when it is indeed useless -# https://github.com/bitcoin/bitcoin/pull/30451 has been partiall reverted in #7184 and should be re-applied -darwin_CC=env -u C_INCLUDE_PATH -u CPLUS_INCLUDE_PATH $(clang_prog) --target=$(host) \ +# TODO: remove C_INCLUDE_PATH/darwin_env_unset when bitcoin#30451 is fully backported. +# C_INCLUDE_PATH/CPLUS_INCLUDE_PATH leak native-toolchain headers into the +# darwin cross-build and conflict with the SDK headers. Guix exports them +# (contrib/guix/libexec/build.sh), so strip them there with an `env -u` +# prefix. The prefix must not be emitted when the variables aren't set: +# ccache cannot parse a compiler command starting with `env` (it treats +# `env` as the compiler and the absolute clang path as a second source +# file), so an unconditional prefix silently disables ccache for every +# darwin compile. Unlike upstream (bitcoin#30451, which dropped the prefix +# entirely), our Guix environment still sets these variables. +ifneq ($(origin C_INCLUDE_PATH) $(origin CPLUS_INCLUDE_PATH),undefined undefined) +darwin_env_unset=env -u C_INCLUDE_PATH -u CPLUS_INCLUDE_PATH +endif + +darwin_CC=$(darwin_env_unset) $(clang_prog) --target=$(host) \ -isysroot$(OSX_SDK) -nostdlibinc \ -iwithsysroot/usr/include -iframeworkwithsysroot/System/Library/Frameworks -# TODO: remove C_INCLUDE_PATH when it is indeed useless -# https://github.com/bitcoin/bitcoin/pull/30451 has been partiall reverted in #7184 and should be re-applied -darwin_CXX=env -u C_INCLUDE_PATH -u CPLUS_INCLUDE_PATH $(clangxx_prog) --target=$(host) \ +darwin_CXX=$(darwin_env_unset) $(clangxx_prog) --target=$(host) \ -isysroot$(OSX_SDK) -nostdlibinc \ -iwithsysroot/usr/include/c++/v1 \ -iwithsysroot/usr/include -iframeworkwithsysroot/System/Library/Frameworks 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.am b/src/Makefile.am index 501a695cf852..30fdf4702361 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -10,8 +10,12 @@ print-%: FORCE DIST_SUBDIRS = secp256k1 AM_LDFLAGS = $(LIBTOOL_LDFLAGS) $(HARDENED_LDFLAGS) $(SANITIZER_LDFLAGS) $(CORE_LDFLAGS) $(BACKTRACE_LDFLAGS) -AM_CFLAGS = $(DEBUG_CFLAGS) $(BACKTRACE_FLAGS) -AM_CXXFLAGS = $(DEBUG_CXXFLAGS) $(HARDENED_CXXFLAGS) $(WARN_CXXFLAGS) $(NOWARN_CXXFLAGS) $(ERROR_CXXFLAGS) $(SANITIZER_CXXFLAGS) $(CORE_CXXFLAGS) $(BACKTRACE_FLAGS) +# BACKTRACE_FLAGS must precede the debug-level flags: -g-family members placed +# there (e.g. a -gdwarf-N version pin) imply -g2, so the intended level +# (DEBUG_*FLAGS' -g1 floor, --enable-debug's -g3, or the user's CXXFLAGS) must +# come later to take effect. +AM_CFLAGS = $(BACKTRACE_FLAGS) $(DEBUG_CFLAGS) +AM_CXXFLAGS = $(BACKTRACE_FLAGS) $(DEBUG_CXXFLAGS) $(HARDENED_CXXFLAGS) $(WARN_CXXFLAGS) $(NOWARN_CXXFLAGS) $(ERROR_CXXFLAGS) $(SANITIZER_CXXFLAGS) $(CORE_CXXFLAGS) AM_OBJCXXFLAGS = $(AM_CXXFLAGS) AM_CPPFLAGS = $(DEBUG_CPPFLAGS) $(HARDENED_CPPFLAGS) $(CORE_CPPFLAGS) AM_LIBTOOLFLAGS = --preserve-dup-deps diff --git a/src/Makefile.test.include b/src/Makefile.test.include index faaee5aa1913..b16f02f9aae3 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -110,6 +110,7 @@ BITCOIN_TESTS =\ test/dynamic_activation_thresholds_tests.cpp \ test/evo_assetlocks_tests.cpp \ test/evo_cbtx_tests.cpp \ + test/evo_db_tests.cpp \ test/evo_deterministicmns_tests.cpp \ test/evo_islock_tests.cpp \ test/evo_mnhf_tests.cpp \ @@ -140,6 +141,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/Makefile.test_util.include b/src/Makefile.test_util.include index 5eae384ebfdc..0f18badb0d0c 100644 --- a/src/Makefile.test_util.include +++ b/src/Makefile.test_util.include @@ -15,6 +15,7 @@ TEST_UTIL_H = \ test/util/index.h \ test/util/llmq_tests.h \ test/util/logging.h \ + test/util/masternode.h \ test/util/mining.h \ test/util/net.h \ test/util/poolresourcetester.h \ @@ -36,6 +37,7 @@ libtest_util_a_SOURCES = \ test/util/coins.cpp \ test/util/json.cpp \ test/util/logging.cpp \ + test/util/masternode.cpp \ test/util/mining.cpp \ test/util/net.cpp \ test/util/script.cpp \ diff --git a/src/active/context.cpp b/src/active/context.cpp index c77e38fb147d..a92c782c3b14 100644 --- a/src/active/context.cpp +++ b/src/active/context.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ ActiveContext::ActiveContext(CBLSWorker& bls_worker, ChainstateManager& chainman const CBLSSecretKey& operator_sk, const util::DbWrapperParams& db_params, bool quorums_watch) : llmq::QuorumRole{qman}, m_bls_worker{bls_worker}, + m_chainman{chainman}, m_quorums_watch{quorums_watch}, nodeman{std::make_unique(connman, dmnman, operator_sk)}, dkgdbgman{std::make_unique(dmnman, qsnapman, chainman)}, @@ -94,6 +96,17 @@ void ActiveContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIn return; nodeman->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); + + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + if (!m_snapshot_duty_blocked.exchange(true)) { + LogPrintf("Masternode DKG participation and quorum signing are disabled until snapshot background validation completes\n"); + } + return; + } + if (m_snapshot_duty_blocked.exchange(false)) { + LogPrintf("Snapshot background validation completed; masternode DKG participation and quorum signing are enabled\n"); + } + ehf_sighandler->UpdatedBlockTip(pindexNew); gov_signer->UpdatedBlockTip(pindexNew); qdkgsman->UpdatedBlockTip(pindexNew, fInitialDownload); diff --git a/src/active/context.h b/src/active/context.h index 75d092920c4a..ecff35a188cf 100644 --- a/src/active/context.h +++ b/src/active/context.h @@ -12,6 +12,7 @@ #include #include +#include #include class CActiveMasternodeManager; @@ -49,7 +50,9 @@ struct DbWrapperParams; struct ActiveContext final : public llmq::QuorumRole, public CValidationInterface { private: CBLSWorker& m_bls_worker; + ChainstateManager& m_chainman; const bool m_quorums_watch{false}; + std::atomic_bool m_snapshot_duty_blocked{false}; public: ActiveContext() = delete; diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 8ea565e8f53f..56cfedf0d42f 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace llmq { ActiveDKGSessionHandler::ActiveDKGSessionHandler( @@ -41,6 +42,8 @@ ActiveDKGSessionHandler::~ActiveDKGSessionHandler() = default; void ActiveDKGSessionHandler::UpdatedBlockTip(const CBlockIndex* pindexNew) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) return; + //AssertLockNotHeld(cs_main); //Indexed quorums (greater than 0) are enabled with Quorum Rotation if (quorumIndex > 0 && !IsQuorumRotationEnabled(params, pindexNew)) { @@ -76,6 +79,10 @@ std::pair ActiveDKGSessionHandler::GetPhaseAndQuorumHash() bool ActiveDKGSessionHandler::InitNewQuorum(gsl::not_null pQuorumBaseBlockIndex) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + return false; + } if (!DeploymentDIP0003Enforced(pQuorumBaseBlockIndex->nHeight, Params().GetConsensus())) { return false; } @@ -100,6 +107,10 @@ void ActiveDKGSessionHandler::WaitForNextPhase(std::optional curPha LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, curPhase.has_value() ? std23::to_underlying(*curPhase) : -1, std23::to_underlying(nextPhase)); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -139,6 +150,10 @@ void ActiveDKGSessionHandler::WaitForNewQuorum(const uint256& oldQuorumHash) con LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d]- starting\n", __func__, params.name, quorumIndex); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -186,6 +201,10 @@ void ActiveDKGSessionHandler::SleepBeforePhase(QuorumPhase curPhase, const uint2 LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting sleep for %d ms, curPhase=%d\n", __func__, params.name, quorumIndex, sleepTime, std23::to_underlying(curPhase)); while (SteadyClock::now() < endTime) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -220,6 +239,10 @@ void ActiveDKGSessionHandler::HandlePhase(QuorumPhase curPhase, QuorumPhase next LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, std23::to_underlying(curPhase), std23::to_underlying(nextPhase)); SleepBeforePhase(curPhase, expectedQuorumHash, randomSleepFactor, runWhileWaiting); + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + throw AbortPhaseException(); + } startPhaseFunc(); WaitForNextPhase(curPhase, nextPhase, expectedQuorumHash, runWhileWaiting); diff --git a/src/active/masternode.cpp b/src/active/masternode.cpp index 4f3979dc629a..7565c013e2a9 100644 --- a/src/active/masternode.cpp +++ b/src/active/masternode.cpp @@ -178,8 +178,21 @@ void CActiveMasternodeManager::UpdatedBlockTip(const CBlockIndex* pindexNew, con const auto [cur_state, cur_protx_hash] = WITH_READ_LOCK(cs, return std::make_pair(m_state, m_protx_hash)); if (cur_state == MasternodeState::READY) { - auto oldMNList = m_dmnman.GetListForBlock(pindexNew->pprev); - auto newMNList = m_dmnman.GetListForBlock(pindexNew); + CDeterministicMNList oldMNList; + CDeterministicMNList newMNList; + try { + oldMNList = m_dmnman.GetListForBlock(pindexNew->pprev); + newMNList = m_dmnman.GetListForBlock(pindexNew); + } catch (const BlockDataUnavailableError& e) { + // This callback runs on the scheduler thread, where an uncaught + // exception terminates the node. Unavailable history is expected + // while a snapshot's background chainstate is still catching up, so + // skip this tip update and let the next one retry. Any other + // exception means local EvoDB/list corruption and must not be + // hidden, so it deliberately stays unhandled. + LogPrintf("CActiveMasternodeManager::%s -- masternode list unavailable: %s\n", __func__, e.what()); + return; + } auto reset = [this, pindexNew](MasternodeState state) -> void { LOCK(cs); m_state = state; diff --git a/src/chainlock/signing.cpp b/src/chainlock/signing.cpp index b9e2f04c5fb2..3fe4f4e6e413 100644 --- a/src/chainlock/signing.cpp +++ b/src/chainlock/signing.cpp @@ -174,7 +174,7 @@ void ChainLockSigner::TrySignChainTip() lastSignedMsgHash = msgHash; } - m_shareman.AsyncSignIfMember(Params().GetConsensus().llmqTypeChainLocks, m_sigman, requestId, msgHash); + m_shareman.AsyncSignIfMember(Params().GetConsensus().llmqTypeChainLocks, requestId, msgHash); } void ChainLockSigner::BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) diff --git a/src/coinjoin/server.cpp b/src/coinjoin/server.cpp index bc0dd30133df..6d9a3ba16578 100644 --- a/src/coinjoin/server.cpp +++ b/src/coinjoin/server.cpp @@ -283,6 +283,7 @@ void CCoinJoinServer::SetNull() AssertLockHeld(cs_coinjoin); // MN side vecSessionCollaterals.clear(); + setSessionCollateralPrevouts.clear(); CCoinJoinBaseSession::SetNull(); m_queueman.SetNull(); @@ -743,6 +744,15 @@ bool CCoinJoinServer::IsAcceptableDSA(const CCoinJoinAccept& dsa, PoolMessage& n return true; } +void CCoinJoinServer::CommitSessionCollateral(const CMutableTransaction& txCollateral) +{ + AssertLockHeld(cs_coinjoin); + vecSessionCollaterals.push_back(MakeTransactionRef(txCollateral)); + for (const auto& txin : txCollateral.vin) { + setSessionCollateralPrevouts.insert(txin.prevout); + } +} + bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) { if (nSessionID != 0) return false; @@ -758,12 +768,26 @@ bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& return false; } - // start new session - nMessageIDRet = MSG_NOERR; - nSessionID = GetRand(/*nMax=*/999999) + 1; - nSessionDenom = dsa.nDenom; + { + LOCK(cs_coinjoin); + + // A scheduler-thread timeout can reset the session via SetNull() between the checks + // above and taking cs_coinjoin, so revalidate: the session state and the collateral + // that opened it have to be committed as one unit. + if (nSessionID != 0 || nState != POOL_STATE_IDLE) { + nMessageIDRet = ERR_MODE; + return false; + } + + // start new session + nMessageIDRet = MSG_NOERR; + nSessionID = GetRand(/*nMax=*/999999) + 1; + nSessionDenom = dsa.nDenom; - SetState(POOL_STATE_QUEUE); + SetState(POOL_STATE_QUEUE); + + CommitSessionCollateral(dsa.txCollateral); + } if (!fUnitTest) { //broadcast that I'm accepting entries, only if it's the first entry through @@ -775,7 +799,6 @@ bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& m_queueman.AddQueue(std::move(dsq)); } - vecSessionCollaterals.push_back(MakeTransactionRef(dsa.txCollateral)); LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- new session created, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d CoinJoin::GetMaxPoolParticipants(): %d\n", nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom), vecSessionCollaterals.size(), CoinJoin::GetMaxPoolParticipants()); @@ -804,10 +827,32 @@ bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolM return false; } + LOCK(cs_coinjoin); + + // A scheduler-thread timeout can reset the session via SetNull() between the checks above + // and taking cs_coinjoin, so revalidate: a collateral must never be committed to a session + // that no longer exists. + if (nSessionID == 0 || nState != POOL_STATE_QUEUE) { + nMessageIDRet = ERR_MODE; + return false; + } + + // Session collaterals are only ever test-accepted, never added to the mempool, so nothing + // pins their identity: the same UTXO can be re-signed into arbitrarily many distinct txids. + // Match on input prevouts so a resent or replayed dsa cannot be counted as a new participant. + for (const auto& txin : dsa.txCollateral.vin) { + if (setSessionCollateralPrevouts.contains(txin.prevout)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- collateral %s spends prevout %s already committed to this session\n", + dsa.txCollateral.GetHash().ToString(), txin.prevout.ToStringShort()); + nMessageIDRet = ERR_ALREADY_HAVE; + return false; + } + } + // count new user as accepted to an existing session nMessageIDRet = MSG_NOERR; - vecSessionCollaterals.push_back(MakeTransactionRef(dsa.txCollateral)); + CommitSessionCollateral(dsa.txCollateral); LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- new user accepted, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d CoinJoin::GetMaxPoolParticipants(): %d\n", nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom), vecSessionCollaterals.size(), CoinJoin::GetMaxPoolParticipants()); @@ -959,7 +1004,7 @@ bool CCoinJoinServer::AlreadyHave(const CInv& inv) return (inv.type == MSG_DSQ) ? m_queueman.HasQueue(inv.hash) : false; } -bool CCoinJoinServer::ProcessGetData(CNode& pfrom, const CInv& inv, CConnman& connman, const CNetMsgMaker& msgMaker) +bool CCoinJoinServer::ProcessGetData(CNode& pfrom, const CInv& inv, const CNetMsgMaker& msgMaker) { if (inv.type != MSG_DSQ) return false; diff --git a/src/coinjoin/server.h b/src/coinjoin/server.h index 3e7443dbc6f0..6e148871b9d0 100644 --- a/src/coinjoin/server.h +++ b/src/coinjoin/server.h @@ -10,6 +10,9 @@ #include #include #include +#include + +#include class CActiveMasternodeManager; class CConnman; @@ -43,6 +46,9 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler // Mixing uses collateral transactions to trust parties entering the pool // to behave honestly. If they don't it takes their money. std::vector vecSessionCollaterals; + // Input prevouts of every transaction in vecSessionCollaterals, so a dsa whose collateral + // reuses one of them can be rejected without rescanning them all. + std::unordered_set setSessionCollateralPrevouts GUARDED_BY(cs_coinjoin); bool fUnitTest; @@ -66,8 +72,10 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler /// Is this nDenom and txCollateral acceptable? bool IsAcceptableDSA(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) const; - bool CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet); - bool AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet); + /// Record an accepted collateral and index its input prevouts + void CommitSessionCollateral(const CMutableTransaction& txCollateral) EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); + bool CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + bool AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); /// Do we have enough users to take entries? bool IsSessionReady() const; @@ -85,7 +93,7 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler void RelayStatus(PoolStatusUpdate nStatusUpdate, PoolMessage nMessageID = MSG_NOERR) EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); void RelayCompletedTransaction(PoolMessage nMessageID) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); - void ProcessDSACCEPT(CNode& peer, CDataStream& vRecv); + void ProcessDSACCEPT(CNode& peer, CDataStream& vRecv) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); void ProcessDSQUEUE(NodeId from, CDataStream& vRecv); void ProcessDSVIN(CNode& peer, CDataStream& vRecv) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); void ProcessDSSIGNFINALTX(CNode& peer, CDataStream& vRecv) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); @@ -103,7 +111,7 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler ~CCoinJoinServer(); void ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv) override; - bool ProcessGetData(CNode& pfrom, const CInv& inv, CConnman& connman, const CNetMsgMaker& msgMaker) override; + bool ProcessGetData(CNode& pfrom, const CInv& inv, const CNetMsgMaker& msgMaker) override; bool AlreadyHave(const CInv& inv) override; void Schedule(CScheduler& scheduler) override; diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 4060d8138d99..95ef9fb9dca5 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -669,6 +669,22 @@ class CDBTransaction { return parent.Read(ssKey, value); } + /** Read a value only if it is present in this transaction's write set. */ + template + bool ReadPending(const K& key, V& value) { + const CDataStream ssKey = KeyToDataStream(key); + auto it = writes.find(ssKey); + if (it == writes.end()) { + return false; + } + auto* impl = dynamic_cast*>(it->second.get()); + if (!impl) { + throw std::runtime_error("ReadPending called with V != previously written type"); + } + value = impl->value; + return true; + } + template bool Exists(const K& key) { return Exists(KeyToDataStream(key)); diff --git a/src/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index cba0bdc55534..966845f77ce9 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -96,7 +96,10 @@ std::string CAssetLockPayload::ToString() const const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx"; -bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const +template +static bool VerifyAssetUnlockSig(const CAssetUnlockPayload& payload, ScanQuorums&& scan_quorums, + GetQuorum&& get_quorum, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) { // That quourm hash must be active at `requestHeight`, // and at the quorumHash must be active in either the current or previous quorum cycle @@ -110,36 +113,60 @@ bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint // We check all active quorums + 1 the latest inactive const int quorums_to_scan = llmq_params_opt->signingActiveQuorumCount + 1; - const auto quorums = qman.ScanQuorums(llmqType, pindexTip, quorums_to_scan); + const auto quorums = scan_quorums(llmqType, pindexTip, quorums_to_scan); - if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; }); !isActive) { + if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == payload.getQuorumHash(); }); !isActive) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-old-quorum"); } - if (static_cast(pindexTip->nHeight) < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) { + if (static_cast(pindexTip->nHeight) < payload.getRequestedHeight() || pindexTip->nHeight >= payload.getHeightToExpiry()) { LogPrint(BCLog::CREDITPOOL, "Asset unlock tx %d with requested height %d could not be accepted on height: %d\n", - index, requestedHeight, pindexTip->nHeight); + payload.getIndex(), payload.getRequestedHeight(), pindexTip->nHeight); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late"); } - const auto quorum = qman.GetQuorum(llmqType, quorumHash); + const auto quorum = get_quorum(llmqType, payload.getQuorumHash()); // quorum must be valid at this point. Let's check and throw error just in case if (!quorum) { - LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, quorumHash.ToString()); + LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, payload.getQuorumHash().ToString()); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-internal-error"); } - const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index)); + const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, payload.getIndex())); if (const llmq::SignHash signHash(llmqType, quorum->qc->quorumHash, requestId, msgHash); - quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { + payload.getQuorumSig().VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { return true; } return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified"); } -bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state) +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) { + return qman.ScanQuorums(llmq_type, pindex, count); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) { + return qman.GetQuorum(llmq_type, quorum_hash); + }, msgHash, pindexTip, state); +} + +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + AssertLockHeld(::cs_main); + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) NO_THREAD_SAFETY_ANALYSIS { + return qman.ScanQuorums(llmq_type, pindex, count, chain); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) NO_THREAD_SAFETY_ANALYSIS { + return qman.GetQuorum(llmq_type, quorum_hash, chain); + }, msgHash, pindexTip, state); +} + +template +static bool CheckAssetUnlockTxImpl(const BlockManager& blockman, VerifySig&& verify_sig, const CTransaction& tx, + gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) { // Some checks depends from blockchain status also, such as `known indexes` and `withdrawal limits` // They are omitted here and done by CCreditPool @@ -180,7 +207,28 @@ bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager uint256 msgHash = tx_copy.GetHash(); - return assetUnlockTx.VerifySig(qman, msgHash, pindexPrev, state); + return verify_sig(assetUnlockTx, msgHash, pindexPrev, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, + gsl::not_null pindexPrev, const std::optional& indexes, + TxValidationState& state) +{ + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) { + return payload.VerifySig(qman, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) +{ + AssertLockHeld(::cs_main); + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) NO_THREAD_SAFETY_ANALYSIS { + return payload.VerifySig(qman, chain, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); } bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state) diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 6a00f605f2c0..634174e1b0f6 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -10,13 +10,17 @@ #include #include #include +#include +#include #include #include class CBlockIndex; +class CChain; class CRangesSet; class TxValidationState; +extern RecursiveMutex cs_main; // NOLINT(readability-redundant-declaration) struct RPCResult; namespace llmq { class CQuorumManager; @@ -114,6 +118,9 @@ class CAssetUnlockPayload [[nodiscard]] UniValue ToJson() const; bool VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const; + bool VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); // getters uint8_t getVersion() const @@ -156,6 +163,10 @@ class CAssetUnlockPayload bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state); bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); +bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state); #endif // BITCOIN_EVO_ASSETLOCKTX_H diff --git a/src/evo/chainhelper.cpp b/src/evo/chainhelper.cpp index 06a610e92194..acffa3cd2d4a 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -85,7 +85,7 @@ bool CChainstateHelper::RemoveConflictingISLockByTx(const CTransaction& tx) return true; } -std::unordered_map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) +std::map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) { return ehf_manager->GetSignalsStage(pindexPrev); } diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index f3d0cbc2c34c..f68c48bd26bf 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -6,9 +6,9 @@ #define BITCOIN_EVO_CHAINHELPER_H #include +#include #include #include -#include class CBlockIndex; class CCreditPoolManager; @@ -77,7 +77,7 @@ class CChainstateHelper bool IsInstantSendWaitingForTx(const uint256& hash) const; bool RemoveConflictingISLockByTx(const CTransaction& tx); - std::unordered_map GetSignalsStage(const CBlockIndex* const pindexPrev); + std::map GetSignalsStage(const CBlockIndex* const pindexPrev); }; #endif // BITCOIN_EVO_CHAINHELPER_H diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 980891fd7c95..5d9a1f60d3cf 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -136,13 +137,22 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool) { + if (height % DISK_SNAPSHOT_PERIOD == 0) { + if (!evoDb.WriteDerived(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { + // A mismatch is local EvoDB corruption, not a statement about the + // block. Abort here: some callers (miner, RPC) never pass through a + // validation-state catch, and the block-connect catches must not + // translate this into a consensus rejection. + const std::string msg = strprintf("CCreditPoolManager::%s -- EvoDB credit pool mismatch for block %s", + __func__, block_hash.ToString()); + AbortNode(msg); + throw EvoDbInconsistencyError(msg); + } + } { LOCK(cache_mutex); creditPoolCache.insert(block_hash, pool); } - if (height % DISK_SNAPSHOT_PERIOD == 0) { - evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool); - } } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) @@ -331,6 +341,11 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo } } return creditPoolDiff; + } catch (const EvoDbInconsistencyError& e) { + // Local EvoDB corruption (the node is already aborting): fail with + // M_ERROR so the block is not marked invalid. + state.Error(e.what()); + return std::nullopt; } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-getcreditpooldiff"); diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 20db7a1f08ec..b25f4d01a514 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -17,6 +17,7 @@ #include #include #include