diff --git a/contrib/containers/guix/scripts/setup-sdk b/contrib/containers/guix/scripts/setup-sdk index 9781ce07285d..6f00e57500b1 100755 --- a/contrib/containers/guix/scripts/setup-sdk +++ b/contrib/containers/guix/scripts/setup-sdk @@ -15,6 +15,35 @@ XCODE_RELEASE="${XCODE_RELEASE:-15A240d}" XCODE_ARCHIVE="Xcode-${XCODE_VERSION}-${XCODE_RELEASE}-extracted-SDK-with-libcxx-headers" XCODE_AR_PATH="${SDK_SOURCES}/${XCODE_ARCHIVE}.tar.gz" +# actions/upload-artifact rejects paths containing characters that are not +# portable across filesystems (notably ':'). The Xcode SDK ships thousands of +# Perl man pages under usr/share/man with '::' in the filename +# (e.g. APR::Base64.3pm). Compilation only needs headers/libs/tbd files; man +# pages are unused by depends/mac and guix darwin builds. Strip them after +# extract so CI can hand the prepared SDK tree to downstream jobs as an +# artifact (and so cache entries stay portable) without packaging changes in +# the pull_request_target caller workflows (which resolve from the default +# branch and cannot be fixed from a release-branch PR head alone). +sanitize_sdk_for_artifacts() { + local sdk_root="$1" + local man_dir="${sdk_root}/usr/share/man" + + if [ -d "${man_dir}" ]; then + echo "Removing SDK man pages incompatible with actions/upload-artifact..." + rm -rf "${man_dir}" + fi + + # Belt-and-suspenders: drop any remaining files whose names contain the + # characters upload-artifact rejects, in case a future SDK adds more. + # See: https://github.com/actions/upload-artifact/issues/546 + if [ -d "${sdk_root}" ]; then + find "${sdk_root}" \( -type f -o -type l \) \ + \( -name '*:*' -o -name '*"*' -o -name '*<*' -o -name '*>*' \ + -o -name '*|*' -o -name '*\**' -o -name '*\?*' \) \ + -print -delete 2>/dev/null || true + fi +} + if [ ! -d "${SDK_PATH}/${XCODE_ARCHIVE}" ]; then if [ ! -f "${XCODE_AR_PATH}" ]; then echo "Downloading macOS SDK..." @@ -24,4 +53,9 @@ if [ ! -d "${SDK_PATH}/${XCODE_ARCHIVE}" ]; then echo "Extracting macOS SDK..." mkdir -p "${SDK_PATH}" tar -C "${SDK_PATH}" -xf "${XCODE_AR_PATH}" + sanitize_sdk_for_artifacts "${SDK_PATH}/${XCODE_ARCHIVE}" +else + # Existing tree (e.g. restored from an older cache that still has man + # pages): ensure it is safe to re-upload if a later step packages it. + sanitize_sdk_for_artifacts "${SDK_PATH}/${XCODE_ARCHIVE}" fi diff --git a/contrib/debian/examples/dash.conf b/contrib/debian/examples/dash.conf index 22965bc6c000..12eaa81441c7 100644 --- a/contrib/debian/examples/dash.conf +++ b/contrib/debian/examples/dash.conf @@ -1 +1,979 @@ -# This is a placeholder file. Please follow the instructions in `contrib/devtools/README.md` to generate a dash.conf file. +## +## dash.conf configuration file. +## Generated by contrib/devtools/gen-dash-conf.sh. +## +## Lines beginning with # are comments. +## All possible configuration options are provided. To use, copy this file +## to your data directory (default or specified by -datadir), uncomment +## options you would like to change, and save the file. +## + + +### Options + + +# Execute command when an alert is raised (%s in cmd is replaced by +# message) +#alertnotify= + +# If this block is in the chain assume that it and its ancestors are valid +# and potentially skip their script verification (0 to verify all, +# default: +# 00000000000000119fe42827219e0686d3f7b494ae65f823194c740c5dbab492, +# testnet: +# 000000541a23f9db7411cddbe50f9f1ebd4aa7108ebdcad62214753f648c0239, +# devnet: +# 0000000000000000000000000000000000000000000000000000000000000000) +#assumevalid= + +# Maintain an index of compact filters by block (default: 0, values: +# basic). If is not supplied or if = 1, indexes for +# all known types are enabled. Automatically enabled for +# masternodes with value 'basic'. +#blockfilterindex= + +# Execute command when the best block changes (%s in cmd is replaced by +# block hash) +#blocknotify= + +# Extra transactions to keep in memory for compact block reconstructions +# (default: 100) +#blockreconstructionextratxn= + +# Specify directory to hold blocks subdirectory for *.dat files (default: +# ) +#blocksdir= + +# Whether to reject transactions from network peers. Automatic broadcast +# and rebroadcast of any transactions from inbound peers is +# disabled, unless the peer has the 'forcerelay' permission. RPC +# transactions are not affected. (default: 0) +#blocksonly=1 + +# Execute command when the best chainlock changes (%s in cmd is replaced +# by chainlocked block hash) +#chainlocknotify= + +# Maintain coinstats index used by the gettxoutsetinfo RPC (default: 0) +#coinstatsindex=1 + +# Specify path to read-only configuration file. Relative paths will be +# prefixed by datadir location (only useable from command line, not +# configuration file) (default: dash.conf) +#conf= + +# Run in the background as a daemon and accept commands (default: 0) +#daemon=1 + +# Wait for initialization to be finished before exiting. This implies +# -daemon (default: 0) +#daemonwait=1 + +# Specify data directory +#datadir= + +# Maximum database cache size MiB (4 to 16384, default: 300). In +# addition, unused mempool memory is shared for this cache (see +# -maxmempool). +#dbcache= + +# Specify location of debug log file. Relative paths will be prefixed by a +# net-specific datadir location. (-nodebuglogfile to disable; +# default: debug.log) +#debuglogfile= + +# Specify additional configuration file, relative to the -datadir path +# (only useable from configuration file, not command line) +#includeconf= + +# Imports blocks from external file on startup +#loadblock= + +# Keep the transaction memory pool below megabytes (default: 300) +#maxmempool= + +# Maximum total size of all orphan transactions in megabytes (default: 10) +#maxorphantxsize= + +# Number of seconds to keep LLMQ recovery sigs (default: 604800) +#maxrecsigsage= + +# Do not keep transactions in the mempool longer than hours (default: +# 336) +#mempoolexpiry= + +# Set the number of script verification threads (0 = auto, <0 = leave that +# many cores free, max: 15, default: 0) +#par= + +# Set the number of BLS verification threads (0 = auto, <0 = leave that +# many cores free, max: 33, default: 0) +#parbls= + +# Whether to save the mempool on shutdown and load on restart (default: 1) +#persistmempool=1 + +# Specify pid file. Relative paths will be prefixed by a net-specific +# datadir location. (default: dashd.pid) +#pid= + +# Reduce storage requirements by enabling pruning (deleting) of old +# blocks. This allows the pruneblockchain RPC to be called to +# delete specific blocks, and enables automatic pruning of old +# blocks if a target size in MiB is provided. This mode is +# incompatible with -txindex, -rescan and -disablegovernance=false. +# Warning: Reverting this setting requires re-downloading the +# entire blockchain. (default: 0 = disable pruning blocks, 1 = +# allow manual pruning via RPC, >945 = automatically prune block +# files to stay under the specified target size in MiB) +#prune= + +# Specify path to dynamic settings data file. Can be disabled with +# -nosettings. File is written at runtime and not meant to be +# edited by users (use dash.conf instead for custom settings). +# Relative paths will be prefixed by datadir location. (default: +# settings.json) +#settings= + +# Execute command immediately before beginning shutdown. The need for +# shutdown may be urgent, so be careful not to delay it long (if +# the command doesn't require interaction with the server, consider +# having it fork into the background). +#shutdownnotify= + +# Execute command on startup. +#startupnotify= + +# Sync mempool from other nodes on start (default: 1) +#syncmempool=1 + +# Print version and exit +#version=1 + + +### Connection options + + +# Add a node to connect to and attempt to keep the connection open (see +# the addnode RPC help for more info). This option can be specified +# multiple times to add multiple nodes; connections are limited to +# 8 at a time and are counted separately from the -maxconnections +# limit. +#addnode= + +# Allow RFC1918 addresses to be relayed and connected to (default: 0) +#allowprivatenet=1 + +# Specify asn mapping used for bucketing of the peers (default: +# ip_asn.map). Relative paths will be prefixed by the net-specific +# datadir location. +#asmap= + +# Default duration (in seconds) of manually configured bans (default: +# 86400) +#bantime= + +# Bind to given address and always listen on it (default: 0.0.0.0). Use +# [host]:port notation for IPv6. Append =onion to tag any incoming +# connections to that address and port as incoming Tor connections +# (default: 127.0.0.1:9996=onion, testnet: 127.0.0.1:19996=onion, +# devnet: 127.0.0.1:19796=onion, regtest: 127.0.0.1:19896=onion) +#bind=[:][=onion] + +# If set, then this host is configured for CJDNS (connecting to fc00::/8 +# addresses would lead us to the CJDNS network, see doc/cjdns.md) +# (default: 0) +#cjdnsreachable=1 + +# Connect only to the specified node; -noconnect disables automatic +# connections (the rules for this peer are the same as for +# -addnode). This option can be specified multiple times to connect +# to multiple nodes. +#connect= + +# Discover own IP addresses (default: 1 when listening and no -externalip +# or -proxy) +#discover=1 + +# Allow DNS lookups for -addnode, -seednode and -connect (default: 1) +#dns=1 + +# Query for peer addresses via DNS lookup, if low on addresses (default: 1 +# unless -connect used or -maxconnections=0) +#dnsseed=1 + +# Specify your own public address +#externalip= + +# Allow fixed seeds if DNS seeds don't provide peers (default: 1) +#fixedseeds=1 + +# Always query for peer addresses via DNS lookup (default: 0) +#forcednsseed=1 + +# Whether to accept inbound I2P connections (default: 1). Ignored if +# -i2psam is not set. Listening for inbound I2P connections is done +# through the SAM proxy, not by binding to a local address and +# port. +#i2pacceptincoming=1 + +# I2P SAM proxy to reach I2P peers and accept I2P connections (default: +# none) +#i2psam= + +# Accept connections from outside (default: 1 if no -proxy, -connect or +# -maxconnections=0) +#listen=1 + +# Automatically create Tor onion service (default: 1) +#listenonion=1 + +# Maintain at most connections to peers (temporary service connections +# excluded) (default: 125). This limit does not apply to +# connections manually added via -addnode or the addnode RPC, which +# have a separate limit of 8. +#maxconnections= + +# Maximum per-connection receive buffer, *1000 bytes (default: 5000) +#maxreceivebuffer= + +# Maximum per-connection memory usage for the send buffer, *1000 bytes +# (default: 1000) +#maxsendbuffer= + +# Maximum allowed median peer time offset adjustment. Local perspective of +# time may be influenced by outbound peers forward or backward by +# this amount (default: 4200 seconds). +#maxtimeadjustment=1 + +# Tries to keep outbound traffic under the given target per 24h. Limit +# does not apply to peers with 'download' permission or blocks +# created within past week. 0 = no limit (default: 0M). Optional +# suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 +# base while uppercase is 1024 base +#maxuploadtarget= + +# Use NAT-PMP to map the listening port (default: 0) +#natpmp=1 + +# Enable all P2P network activity (default: 1). Can be changed by the +# setnetworkactive RPC command +#networkactive=1 + +# Use separate SOCKS5 proxy to reach peers via Tor onion services, set +# -noonion to disable (default: -proxy). May be a local file path +# prefixed with 'unix:'. +#onion= + +# Make automatic outbound connections only to network (ipv4, ipv6, +# onion, i2p, cjdns). Inbound and manual connections are not +# affected by this option. It can be specified multiple times to +# allow multiple networks. +#onlynet= + +# Serve compact block filters to peers per BIP 157 (default: 0, +# automatically enabled for masternodes) +#peerblockfilters=1 + +# Support filtering of blocks and transaction with bloom filters (default: +# 1) +#peerbloomfilters=1 + +# Specify a p2p connection timeout delay in seconds. After connecting to a +# peer, wait this amount of time before considering disconnection +# based on inactivity (minimum: 1, default: 60) +#peertimeout= + +# Listen for connections on . Nodes not using the default ports +# (default: 9999, testnet: 19999, devnet: 19799, regtest: 19899) +# are unlikely to get incoming connections. Not relevant for I2P +# (see doc/i2p.md). +#port= + +# Connect through SOCKS5 proxy, set -noproxy to disable (default: +# disabled). May be a local file path prefixed with 'unix:' if the +# proxy supports it. +#proxy= + +# Randomize credentials for every proxy connection. This enables Tor +# stream isolation (default: 1) +#proxyrandomize=1 + +# Connect to a node to retrieve peer addresses, and disconnect. This +# option can be specified multiple times to connect to multiple +# nodes. +#seednode= + +# Socket events mode, which must be one of 'select', 'poll', 'epoll' or +# 'kqueue', depending on your system (default: Linux - 'epoll', +# FreeBSD/Apple - 'kqueue', Windows - 'select') +#socketevents= + +# Specify socket connection timeout in milliseconds. If an initial attempt +# to connect is unsuccessful after this amount of time, drop it +# (minimum: 1, default: 5000) +#timeout= + +# Tor control host and port to use if onion listening enabled (default: +# 127.0.0.1:9051). If no port is specified, the default port of +# 9051 will be used. +#torcontrol=: + +# Tor control port password (default: empty) +#torpassword= + +# Use UPnP to map the listening port (default: 0) +#upnp=1 + +# Support v2 transport (default: 1) +#v2transport=1 + +# Bind to the given address and add permission flags to the peers +# connecting to it. Use [host]:port notation for IPv6. Allowed +# permissions: bloomfilter (allow requesting BIP37 filtered blocks +# and transactions), noban (do not ban for misbehavior; implies +# download), forcerelay (relay transactions that are already in the +# mempool; implies relay), relay (relay even in -blocksonly mode), +# mempool (allow requesting BIP35 mempool contents), download +# (allow getheaders during IBD, no disconnect after maxuploadtarget +# limit), addr (responses to GETADDR avoid hitting the cache and +# contain random records with the most up-to-date info). Specify +# multiple permissions separated by commas (default: +# download,noban,mempool,relay). Can be specified multiple times. +#whitebind=<[permissions@]addr> + +# Add permission flags to the peers connecting from the given IP address +# (e.g. 1.2.3.4) or CIDR-notated network (e.g. 1.2.3.0/24). Uses +# the same permissions as -whitebind. Can be specified multiple +# times. +#whitelist=<[permissions@]IP address or network> + + +### Indexing options + + +# Maintain a full address index, used to query for the balance, txids and +# unspent outputs for addresses (default: 0) +#addressindex=1 + +# Rebuild chain state and block index from the blk*.dat files on disk. +# This will also rebuild active optional indexes. +#reindex=1 + +# Rebuild chain state from the currently indexed blocks. When in pruning +# mode or if blocks on disk might be corrupted, use full -reindex +# instead. Deactivate all optional indexes before running this. +#reindex-chainstate=1 + +# Maintain a full spent index, used to query the spending txid and input +# index for an outpoint (default: 0) +#spentindex=1 + +# Maintain a timestamp index for block hashes, used to query blocks hashes +# by a range of timestamps (default: 0) +#timestampindex=1 + +# Maintain a full transaction index, used by the getrawtransaction rpc +# call (default: 1) +#txindex=1 + + +### Masternode options + + +# Set the username for the "platform user", a restricted user intended to +# be used by Dash Platform, to the specified username. +#deprecated-platform-user= + +# Enable automated quorum data recovery (default: 1) +#llmq-data-recovery= + +# Defines from which LLMQ type the masternode should sync quorum +# verification vectors. Can be used multiple times with different +# LLMQ types. : 0 (sync always from all quorums of the type +# defined by ), 1 (sync from all quorums of the type +# defined by if a member of any of the quorums) +#llmq-qvvec-sync=: + +# Set the masternode BLS private key and enable the client to act as a +# masternode +#masternodeblsprivkey= + + +### Statsd options + + +# Specify the size of each batch of stats messages (default: 1024) +#statsbatchsize= + +# Specify the number of milliseconds between stats messages (default: +# 1000) +#statsduration= + +# Specify statsd host (default: ) +#statshost= + +# Specify the number of seconds between periodic measurements (default: +# 60) +#statsperiod= + +# Specify an optional string prepended to every stats key (default: ) +#statsprefix= + +# Specify an optional string appended to every stats key (default: ) +#statssuffix= + + +### Wallet options + + +# Group outputs by address, selecting many (possibly all) or none, instead +# of selecting on a per-output basis. Privacy is improved as +# addresses are mostly swept with fewer transactions and outputs +# are aggregated in clean change addresses. It may result in higher +# fees due to less optimal coin selection caused by this added +# limitation and possibly a larger-than-necessary number of inputs +# being used. Always enabled for wallets with "avoid_reuse" +# enabled, otherwise default: 0. +#avoidpartialspends=1 + +# The maximum feerate (in DASH/kvB) at which transaction building may use +# more inputs than strictly necessary so that the wallet's UTXO +# pool can be reduced (default: 0.00001). +#consolidatefeerate= + +# Number of automatic wallet backups (default: 10) +#createwalletbackups= + +# Do not load the wallet and disable wallet RPC calls +#disablewallet=1 + +# Execute command when a wallet InstantSend transaction is successfully +# locked. %s in cmd is replaced by TxID and %w is replaced by +# wallet name. %w is not currently implemented on Windows. On +# systems where %w is supported, it should NOT be quoted because +# this would break shell escaping used to invoke the command. +#instantsendnotify= + +# Set key pool size to (default: 1000). Warning: Smaller sizes may +# increase the risk of losing funds when restoring from an old +# backup, if none of the addresses in the original keypool have +# been used. +#keypool= + +# Spend up to this amount in additional (absolute) fees (in DASH) if it +# allows the use of partial spend avoidance (default: 0.00) +#maxapsfee= + +# Rescan the block chain for missing wallet transactions on startup (1 = +# start from wallet creation time, 2 = start from genesis block) +#rescan= + +# External signing tool, see doc/external-signer.md +#signer= + +# Spend unconfirmed change when sending transactions (default: 1) +#spendzeroconfchange=1 + +# Specify wallet path to load at startup. Can be used multiple times to +# load multiple wallets. Path is to a directory containing wallet +# data and log files. If the path is not absolute, it is +# interpreted relative to . This only loads existing +# wallets and does not create new ones. For backwards compatibility +# this also accepts names of existing top-level data files in +# . +#wallet= + +# Specify full path to directory for automatic wallet backups (must exist) +#walletbackupsdir= + +# Make the wallet broadcast transactions (default: 1) +#walletbroadcast=1 + +# Specify directory to hold wallets (default: /wallets if it +# exists, otherwise ) +#walletdir= + +# Execute command when a wallet transaction changes. %s in cmd is replaced +# by TxID, %w is replaced by wallet name, %b is replaced by the +# hash of the block including the transaction (set to 'unconfirmed' +# if the transaction is not included) and %h is replaced by the +# block height (-1 if not included). %w is not currently +# implemented on windows. On systems where %w is supported, it +# should NOT be quoted because this would break shell escaping used +# to invoke the command. +#walletnotify= + + +### Wallet fee options + + +# The fee rate (in DASH/kB) that indicates your tolerance for discarding +# change by adding it to the fee (default: 0.0001). Note: An output +# is discarded if it is dust at this rate, but we will always +# discard up to the dust relay fee and a discard fee above that is +# limited by the fee estimate for the longest target +#discardfee= + +# A fee rate (in DASH/kB) that will be used when fee estimation has +# insufficient data. 0 to entirely disable the fallbackfee feature. +# (default: 0.00001) +#fallbackfee= + +# Fee rates (in DASH/kB) smaller than this are considered zero fee for +# transaction creation (default: 0.00001) +#mintxfee= + +# Fee rate (in DASH/kB) to add to transactions you send (default: 0.00) +#paytxfee= + +# If paytxfee is not set, include enough fee so transactions begin +# confirmation on average within n blocks (default: 6) +#txconfirmtarget= + + +### HD wallet options + + +# User defined seed for HD wallet (should be in hex). Only has effect +# during wallet creation/first start (default: randomly generated) +#hdseed= + +# User defined mnemonic for HD wallet (bip39). Only has effect during +# wallet creation/first start (default: randomly generated) +#mnemonic= + +# User defined mnemonic security for HD wallet in bits (BIP39). Only has +# effect during wallet creation/first start (allowed values: 128, +# 160, 192, 224, 256; default: 128) +#mnemonicbits= + +# User defined mnemonic passphrase for HD wallet (BIP39). Only has effect +# during wallet creation/first start (default: empty string) +#mnemonicpassphrase= + +# Use hierarchical deterministic key generation (HD) after BIP39/BIP44. +# Only has effect during wallet creation/first start (default: 1) +#usehd=1 + + +### CoinJoin options + + +# Target CoinJoin balance (2-21000000, default: 1000) +#coinjoinamount= + +# Start CoinJoin automatically (0-1, default: 0) +#coinjoinautostart=1 + +# Try to create at least N inputs of each denominated amount (10-100000, +# default: 50) +#coinjoindenomsgoal= + +# Create up to N inputs of each denominated amount (10-100000, default: +# 300) +#coinjoindenomshardcap= + +# Enable multiple CoinJoin mixing sessions per block, experimental (0-1, +# default: 0) +#coinjoinmultisession=1 + +# Use N separate masternodes for each denominated input to mix funds +# (2-16, default: 4) +#coinjoinrounds= + +# Use N separate masternodes in parallel to mix funds (1-10, default: 4) +#coinjoinsessions= + +# Enable use of CoinJoin for funds stored in this wallet (0-1, default: 0) +#enablecoinjoin=1 + + +### ZeroMQ notification options + + +# Enable publish hash block in
+#zmqpubhashblock=
+ +# Set publish hash block outbound message high water mark (default: 1000) +#zmqpubhashblockhwm= + +# Enable publish hash block (locked via ChainLocks) in
+#zmqpubhashchainlock=
+ +# Set publish hash chain lock outbound message high water mark (default: +# 1000) +#zmqpubhashchainlockhwm= + +# Enable publish hash of governance objects (like proposals) in
+#zmqpubhashgovernanceobject=
+ +# Set publish hash governance object outbound message high water mark +# (default: 1000) +#zmqpubhashgovernanceobjecthwm= + +# Enable publish hash of governance votes in
+#zmqpubhashgovernancevote=
+ +# Set publish hash governance vote outbound message high water mark +# (default: 1000) +#zmqpubhashgovernancevotehwm= + +# Enable publish transaction hashes of attempted InstantSend double spend +# in
+#zmqpubhashinstantsenddoublespend=
+ +# Set publish hash InstantSend double spend outbound message high water +# mark (default: 1000) +#zmqpubhashinstantsenddoublespendhwm= + +# Enable publish message hash of recovered signatures (recovered by LLMQs) +# in
+#zmqpubhashrecoveredsig=
+ +# Set publish hash recovered signature outbound message high water mark +# (default: 1000) +#zmqpubhashrecoveredsighwm= + +# Enable publish hash transaction in
+#zmqpubhashtx=
+ +# Set publish hash transaction outbound message high water mark (default: +# 1000) +#zmqpubhashtxhwm= + +# Enable publish hash transaction (locked via InstantSend) in
+#zmqpubhashtxlock=
+ +# Set publish hash transaction lock outbound message high water mark +# (default: 1000) +#zmqpubhashtxlockhwm= + +# Enable publish raw block in
+#zmqpubrawblock=
+ +# Set publish raw block outbound message high water mark (default: 1000) +#zmqpubrawblockhwm= + +# Enable publish raw block (locked via ChainLocks) in
+#zmqpubrawchainlock=
+ +# Set publish raw chain lock outbound message high water mark (default: +# 1000) +#zmqpubrawchainlockhwm= + +# Enable publish raw block (locked via ChainLocks) and CLSIG message in +#
+#zmqpubrawchainlocksig=
+ +# Set publish raw chain lock signature outbound message high water mark +# (default: 1000) +#zmqpubrawchainlocksighwm= + +# Enable publish raw governance votes in
+#zmqpubrawgovernanceobject=
+ +# Set publish raw governance object outbound message high water mark +# (default: 1000) +#zmqpubrawgovernanceobjecthwm= + +# Enable publish raw governance objects (like proposals) in
+#zmqpubrawgovernancevote=
+ +# Set publish raw governance vote outbound message high water mark +# (default: 1000) +#zmqpubrawgovernancevotehwm= + +# Enable publish raw transactions of attempted InstantSend double spend in +#
+#zmqpubrawinstantsenddoublespend=
+ +# Set publish raw InstantSend double spend outbound message high water +# mark (default: 1000) +#zmqpubrawinstantsenddoublespendhwm= + +# Enable publish raw recovered signatures (recovered by LLMQs) in +#
+#zmqpubrawrecoveredsig=
+ +# Set publish raw recovered signature outbound message high water mark +# (default: 1000) +#zmqpubrawrecoveredsighwm= + +# Enable publish raw transaction in
+#zmqpubrawtx=
+ +# Set publish raw transaction outbound message high water mark (default: +# 1000) +#zmqpubrawtxhwm= + +# Enable publish raw transaction (locked via InstantSend) in
+#zmqpubrawtxlock=
+ +# Set publish raw transaction lock outbound message high water mark +# (default: 1000) +#zmqpubrawtxlockhwm= + +# Enable publish raw transaction (locked via InstantSend) and ISLOCK in +#
+#zmqpubrawtxlocksig=
+ +# Set publish raw transaction lock signature outbound message high water +# mark (default: 1000) +#zmqpubrawtxlocksighwm= + +# Enable publish hash block and tx sequence in
+#zmqpubsequence=
+ +# Set publish hash sequence message high water mark (default: 1000) +#zmqpubsequencehwm= + + +### Debugging/Testing options + + +# Output debug and trace logging (default: -nodebug, supplying +# is optional). If is not supplied or if = 1, +# output all debug and trace logging. can be: addrman, +# bench, blockstorage, chainlocks, cmpctblock, coindb, coinjoin, +# creditpool, ehf, estimatefee, gobject, http, i2p, instantsend, +# ipc, leveldb, libevent, llmq, llmq-dkg, llmq-sigs, mempool, +# mempoolrej, mnpayments, mnsync, net, netconn, proxy, prune, qt, +# rand, reindex, rpc, selectcoins, spork, tor, txreconciliation, +# validation, walletdb, zmq. This option can be specified multiple +# times to output multiple categories. +#debug= + +# Exclude debug and trace logging for a category. Can be used in +# conjunction with -debug=1 to output debug and trace logging for +# all categories except the specified category. This option can be +# specified multiple times to exclude multiple categories. +#debugexclude= + +# Disable governance validation (0-1, default: 0) +#disablegovernance=1 + +# Print help message with debugging options and exit +#help-debug=1 + +# Include IP addresses in debug output (default: 0) +#logips=1 + +# Always prepend a category and level (default: 0) +#loglevelalways=1 + +# Prepend debug output with name of the originating source location +# (source file, line number and function name) (default: 0) +#logsourcelocations=1 + +# Prepend debug output with timestamp (default: 1) +#logtimestamps=1 + +# Maximum total fees (in DASH) to use in a single wallet transaction; +# setting this too low may abort large transactions (default: 0.10) +#maxtxfee= + +# Overrides minimum spork signers to change spork value. Only useful for +# regtest and devnet. Using this on mainnet or testnet will ban +# you. +#minsporkkeys= + +# Send trace/debug info to console (default: 1 when no -daemon. To disable +# logging to file, set -nodebuglogfile) +#printtoconsole=1 + +# Protocol version to report to other nodes +#pushversion=1 + +# Shrink debug.log file on client startup (default: 1 when no -debug) +#shrinkdebugfile=1 + +# Override spork address. Only useful for regtest and devnet. Using this +# on mainnet or testnet will ban you. +#sporkaddr= + +# Set the private key to be used for signing spork messages. +#sporkkey= + +# Append comment to the user agent string +#uacomment= + + +### Chain selection options + + +# Use the chain (default: main). Allowed values: main, test, +# devnet, regtest +#chain= + +# Use devnet chain with provided name +#devnet= + +# The number of blocks with a higher than normal subsidy to mine at the +# start of a chain. Block after that height will have fixed subsidy +# base. (default: 0, devnet-only) +#highsubsidyblocks= + +# The factor to multiply the normal block subsidy by while in the +# highsubsidyblocks window of a chain (default: 1, devnet-only) +#highsubsidyfactor= + +# Override the default LLMQ type used for ChainLocks. Allows using +# ChainLocks with smaller LLMQs. (default: llmq_devnet, +# devnet-only) +#llmqchainlocks= + +# Override the default LLMQ size for the LLMQ_DEVNET quorum (devnet-only) +#llmqdevnetparams=: + +# Override the default LLMQ type used for InstantSendDIP0024. (default: +# llmq_devnet_dip0024, devnet-only) +#llmqinstantsenddip0024= + +# Override the default LLMQ type used for EHF. (default: llmq_devnet, +# devnet-only) +#llmqmnhf= + +# Override the default LLMQ type used for Platform. (default: +# llmq_devnet_platform, devnet-only) +#llmqplatform= + +# The number of blocks that can be mined with the minimum difficulty at +# the start of a chain (default: 0, devnet-only) +#minimumdifficultyblocks= + +# Override the default PowTargetSpacing value in seconds (default: 2.5 +# minutes, devnet-only) +#powtargetspacing= + +# Use the test chain. Equivalent to -chain=test +#testnet=1 + + +### Node relay options + + +# Equivalent bytes per sigop in transactions for relay and mining +# (default: 20) +#bytespersigop=1 + +# Relay and mine data carrier transactions (default: 1) +#datacarrier=1 + +# Maximum size of data in data carrier transactions we relay and mine +# (default: 83) +#datacarriersize=1 + +# Fees (in DASH/kB) smaller than this are considered zero fee for +# relaying, mining and transaction creation (default: 0.00001) +#minrelaytxfee= + +# Relay non-P2SH multisig (default: 1) +#permitbaremultisig=1 + +# Add 'forcerelay' permission to whitelisted inbound peers with default +# permissions. This will relay transactions even if the +# transactions were already in the mempool. (default: 0) +#whitelistforcerelay=1 + +# Add 'relay' permission to whitelisted inbound peers with default +# permissions. This will accept relayed transactions even when not +# relaying transactions (default: 1) +#whitelistrelay=1 + + +### Block creation options + + +# Set maximum block size in bytes (default: 2000000) +#blockmaxsize= + +# Set lowest fee rate (in DASH/kB) for transactions to be included in +# block creation. (default: 0.00001) +#blockmintxfee= + + +### RPC server options + + +# Accept public REST requests (default: 0) +#rest=1 + +# Allow JSON-RPC connections from specified source. Valid values for +# are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. +# 1.2.3.4/255.255.255.0), a network/CIDR (e.g. 1.2.3.4/24), all +# ipv4 (0.0.0.0/0), or all ipv6 (::/0). This option can be +# specified multiple times +#rpcallowip= + +# Username and HMAC-SHA-256 hashed password for JSON-RPC connections. The +# field comes in the format: :$. A +# canonical python script is included in share/rpcuser. The client +# then connects normally using the +# rpcuser=/rpcpassword= pair of arguments. This +# option can be specified multiple times +#rpcauth= + +# Bind to given address to listen for JSON-RPC connections. Do not expose +# the RPC server to untrusted networks such as the public internet! +# This option is ignored unless -rpcallowip is also passed. Port is +# optional and overrides -rpcport. Use [host]:port notation for +# IPv6. This option can be specified multiple times (default: +# 127.0.0.1 and ::1 i.e., localhost, or if -rpcallowip has been +# specified, 0.0.0.0 and :: i.e., all addresses) +#rpcbind=[:port] + +# Location of the auth cookie. Relative paths will be prefixed by a +# net-specific datadir location. (default: data dir) +#rpccookiefile= + +# List of comma-separated usernames for JSON-RPC external connections +#rpcexternaluser= + +# Password for JSON-RPC connections +#rpcpassword= + +# Listen for JSON-RPC connections on (default: 9998, testnet: +# 19998, devnet: 19798, regtest: 19898) +#rpcport= + +# Set the number of threads to service RPC calls (default: 4) +#rpcthreads= + +# Username for JSON-RPC connections +#rpcuser= + +# Set a whitelist to filter incoming RPC calls for a specific user. The +# field comes in the format: :,,...,. If multiple whitelists are set for a given user, +# they are set-intersected. See -rpcwhitelistdefault documentation +# for information on default whitelist behavior. +#rpcwhitelist= + +# Sets default behavior for rpc whitelisting. Unless rpcwhitelistdefault +# is set to 0, if any -rpcwhitelist is set, the rpc server acts as +# if all rpc users are subject to empty-unless-otherwise-specified +# whitelists. If rpcwhitelistdefault is set to 1 and no +# -rpcwhitelist is set, rpc server acts as if all rpc users are +# subject to empty whitelists. +#rpcwhitelistdefault=1 + +# Accept command line and JSON-RPC commands +#server=1 + + +# [Sections] +# Most options will apply to all networks. To confine an option to a specific +# network, add it under the relevant section below. +# +# Note: If not specified under a network section, the options addnode, connect, +# port, bind, rpcport, rpcbind, and wallet will only apply to mainnet. + +# Options for mainnet +[main] + +# Options for testnet +[test] + +# Options for regtest +[regtest] diff --git a/doc/release-notes.md b/doc/release-notes.md index 4bd06422ada9..0a472cf126f8 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -26,7 +26,7 @@ require a reindex. # Release Notes -## Security +## Security and P2P hardening 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 @@ -64,6 +64,19 @@ masternodes in particular - so upgrading is recommended. (dash#7398), including detection of mutated blocks as a defence-in-depth measure. +## CoinJoin and wallet + +- Fixed CoinJoin client lifetime handling by executing client callbacks while + the wallet-manager map remains locked, avoiding dangling client pointers + during wallet removal (dash#7259). +- CoinJoin initialization now follows wallet addition, the configured CoinJoin + preference remains consistent across UI and command-line paths, and locked + wallets are no longer automatically started for mixing. Re-adding an + already-registered wallet no longer reinitializes it or autostarts mixing. +- Moved mixing state onto the wallet to avoid a wallet-manager lock-order + deadlock, with regression coverage for new-keypool callbacks and the + wallet-manager lock-order cycle. + ## RPC - `protx listdiff` no longer reports an always-zero `platformP2PPort` / @@ -77,6 +90,8 @@ masternodes in particular - so upgrading is recommended. - 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). +- The CoinJoin toggle now reflects externally started mixing and remains on the + Start action when a local start attempt fails. ## Build and CI @@ -100,7 +115,9 @@ See detailed [set of changes][set-of-changes]. Thanks to everyone who directly contributed to this release: +- Claude Code - Konstantin Akimov +- MarcoFalke - PastaClaw - PastaPastaPasta - UdjinM6 diff --git a/src/active/quorums.cpp b/src/active/quorums.cpp index 9103207612b9..f21225d8be11 100644 --- a/src/active/quorums.cpp +++ b/src/active/quorums.cpp @@ -24,6 +24,8 @@ #include +#include + namespace llmq { QuorumParticipant::QuorumParticipant(CBLSWorker& bls_worker, CConnman& connman, CDeterministicMNManager& dmnman, QuorumObserverParent& qman, CQuorumSnapshotManager& qsnapman, diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 4cb1c9b7b2d2..de9ded07eb76 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -1913,12 +1913,13 @@ CoinJoinWalletManager::~CoinJoinWalletManager() } } -void CoinJoinWalletManager::Add(const std::shared_ptr& wallet) +bool CoinJoinWalletManager::Add(const std::shared_ptr& wallet) { LOCK(cs_wallet_manager_map); - m_wallet_manager_map.try_emplace(wallet->GetName(), - std::make_unique(wallet, m_dmnman, m_mn_metaman, m_mn_sync, - m_isman, m_queueman)); + return m_wallet_manager_map.try_emplace(wallet->GetName(), + std::make_unique(wallet, m_dmnman, m_mn_metaman, + m_mn_sync, m_isman, m_queueman)) + .second; } void CoinJoinWalletManager::DoMaintenance(CConnman& connman) @@ -1933,4 +1934,3 @@ void CoinJoinWalletManager::Remove(const std::string& name) { LOCK(cs_wallet_manager_map); m_wallet_manager_map.erase(name); } - diff --git a/src/coinjoin/client.h b/src/coinjoin/client.h index 0aba5675b521..9176d5c7677a 100644 --- a/src/coinjoin/client.h +++ b/src/coinjoin/client.h @@ -84,7 +84,7 @@ class CoinJoinWalletManager { const std::unique_ptr& queueman); ~CoinJoinWalletManager(); - void Add(const std::shared_ptr& wallet) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map); + bool Add(const std::shared_ptr& wallet) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map); void DoMaintenance(CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map); void Remove(const std::string& name) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet_manager_map); diff --git a/src/coinjoin/interfaces.cpp b/src/coinjoin/interfaces.cpp index 5ddb127f4217..50488450a2d1 100644 --- a/src/coinjoin/interfaces.cpp +++ b/src/coinjoin/interfaces.cpp @@ -37,8 +37,9 @@ class CoinJoinLoaderImpl : public interfaces::CoinJoin::Loader void AddWallet(const std::shared_ptr& wallet) override { - manager().addWallet(wallet); - if (!CCoinJoinClientOptions::IsEnabled()) return; + // Skip (re-)initialization when the wallet was already registered: a + // duplicate AddWallet must not reinitialize settings or autostart mixing. + if (!manager().addWallet(wallet) || !CCoinJoinClientOptions::IsEnabled()) return; manager().doForClient(wallet->GetName(), [](CCoinJoinClientManager& mgr) { g_wallet_init_interface.InitCoinJoinSettings(mgr); }); diff --git a/src/coinjoin/walletman.cpp b/src/coinjoin/walletman.cpp index b80fd4e0bba4..9748cfc6935a 100644 --- a/src/coinjoin/walletman.cpp +++ b/src/coinjoin/walletman.cpp @@ -40,7 +40,7 @@ class CJWalletManagerImpl final : public CJWalletManager std::optional getQueueFromHash(const uint256& hash) const override; std::optional getQueueSize() const override; std::vector getMixingMasternodes() override; - void addWallet(const std::shared_ptr& wallet) override; + bool addWallet(const std::shared_ptr& wallet) override; void removeWallet(const std::string& name) override; void flushWallet(const std::string& name) override; @@ -133,9 +133,9 @@ std::vector CJWalletManagerImpl::getMixingMasternodes() return ret; } -void CJWalletManagerImpl::addWallet(const std::shared_ptr& wallet) +bool CJWalletManagerImpl::addWallet(const std::shared_ptr& wallet) { - walletman.Add(wallet); + return walletman.Add(wallet); } void CJWalletManagerImpl::flushWallet(const std::string& name) diff --git a/src/coinjoin/walletman.h b/src/coinjoin/walletman.h index 5979cdc048f9..ec3075be93d0 100644 --- a/src/coinjoin/walletman.h +++ b/src/coinjoin/walletman.h @@ -56,7 +56,7 @@ class CJWalletManager : public CValidationInterface virtual std::optional getQueueFromHash(const uint256& hash) const = 0; virtual std::optional getQueueSize() const = 0; virtual std::vector getMixingMasternodes() = 0; - virtual void addWallet(const std::shared_ptr& wallet) = 0; + virtual bool addWallet(const std::shared_ptr& wallet) = 0; virtual void removeWallet(const std::string& name) = 0; virtual void flushWallet(const std::string& name) = 0; diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index 91a19b4755e2..49958d785ae7 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -87,29 +87,27 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const if (fields & Field_platformNodeID) { obj.pushKV("platformNodeID", state.platformNodeID.ToString()); } - // v23.1.x adaptation: upstream gates these deprecated fields behind - // IsServiceDeprecatedRPCEnabled(), but on this branch they deliberately - // remain unenforced through gating (see bbcd9d543e6) and were always - // returned in v23.1.7. Keep `if (true)` so the block structure stays - // aligned with develop for future backports. - if (true) { - // platformP2PPort/platformHTTPPort are deprecated scalar duplicates of netInfo's - // Platform entries. From ExtAddr onwards the scalar fields are unused (always 0), so - // when the diff carries an ExtAddr netInfo report the live port from it to stay - // consistent with the "addresses" output below. - const bool has_ext_netinfo = (fields & Field_netInfo) && state.netInfo->CanStorePlatform(); - if (fields & Field_platformP2PPort) { - obj.pushKV("platformP2PPort", - has_ext_netinfo && state.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P) - ? state.netInfo->GetEntries(NetInfoPurpose::PLATFORM_P2P)[0].GetPort() - : state.platformP2PPort); - } - if (fields & Field_platformHTTPPort) { - obj.pushKV("platformHTTPPort", - has_ext_netinfo && state.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) - ? state.netInfo->GetEntries(NetInfoPurpose::PLATFORM_HTTPS)[0].GetPort() - : state.platformHTTPPort); - } + // platformP2PPort/platformHTTPPort are deprecated scalar duplicates of netInfo's + // Platform entries. From ExtAddr onwards the scalar fields are unused (always 0), so + // when the diff carries an ExtAddr netInfo report the live port from it to stay + // consistent with the "addresses" output below. + // + // NOTE: unlike develop, v23.1.x does not gate these fields behind + // -deprecatedrpc=service. That gating was removed in bbcd9d543e6 and only restored + // upstream by #7181, which targets the v24 line and is not part of this release. + // Gating here would drop the fields entirely for every default node. + const bool has_ext_netinfo = (fields & Field_netInfo) && state.netInfo->CanStorePlatform(); + if (fields & Field_platformP2PPort) { + obj.pushKV("platformP2PPort", + has_ext_netinfo && state.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P) + ? state.netInfo->GetEntries(NetInfoPurpose::PLATFORM_P2P)[0].GetPort() + : state.platformP2PPort); + } + if (fields & Field_platformHTTPPort) { + obj.pushKV("platformHTTPPort", + has_ext_netinfo && state.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) + ? state.netInfo->GetEntries(NetInfoPurpose::PLATFORM_HTTPS)[0].GetPort() + : state.platformHTTPPort); } } { diff --git a/src/governance/net_governance.cpp b/src/governance/net_governance.cpp index 8b551df9bd4f..3ab3ae35f777 100644 --- a/src/governance/net_governance.cpp +++ b/src/governance/net_governance.cpp @@ -270,3 +270,35 @@ void NetGovernance::ProcessMessage(CNode& peer, const std::string& msg_type, CDa } } } + +bool NetGovernance::AlreadyHave(const CInv& inv) +{ + if (inv.type != MSG_GOVERNANCE_OBJECT && inv.type != MSG_GOVERNANCE_OBJECT_VOTE) { + return false; + } + // When governance isn't loaded (e.g. -disablegovernance), claim we already have + // the item so we don't fetch or track it in the net-layer request tracker. + if (!m_gov_manager.IsValid()) return true; + return !m_gov_manager.ConfirmInventoryRequest(inv); +} + +bool NetGovernance::ProcessGetData(CNode& pfrom, const CInv& inv, CConnman& connman, const CNetMsgMaker& msgMaker) +{ + if (inv.type == MSG_GOVERNANCE_OBJECT) { + if (!m_gov_manager.HaveObjectForHash(inv.hash)) return false; + CDataStream ss(SER_NETWORK, pfrom.GetCommonVersion()); + ss.reserve(1000); + if (!m_gov_manager.SerializeObjectForHash(inv.hash, ss)) return false; + connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::MNGOVERNANCEOBJECT, ss)); + return true; + } + if (inv.type == MSG_GOVERNANCE_OBJECT_VOTE) { + if (!m_gov_manager.HaveVoteForHash(inv.hash)) return false; + CDataStream ss(SER_NETWORK, pfrom.GetCommonVersion()); + ss.reserve(1000); + if (!m_gov_manager.SerializeVoteForHash(inv.hash, ss)) return false; + connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::MNGOVERNANCEOBJECTVOTE, ss)); + return true; + } + return false; +} diff --git a/src/governance/net_governance.h b/src/governance/net_governance.h index 6d226f5f8f53..5eaf4155b01a 100644 --- a/src/governance/net_governance.h +++ b/src/governance/net_governance.h @@ -27,6 +27,9 @@ class NetGovernance final : public NetHandler void ProcessMessage(CNode& peer, const std::string& msg_type, CDataStream& vRecv) override; + bool AlreadyHave(const CInv& inv) override; + bool ProcessGetData(CNode& pfrom, const CInv& inv, CConnman& connman, const CNetMsgMaker& msgMaker) override; + private: CGovernanceManager& m_gov_manager; CMasternodeSync& m_node_sync; diff --git a/src/init.cpp b/src/init.cpp index 20e9a5f4ac00..6d874bfc3e52 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2257,8 +2257,12 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } return InitError(strprintf(_("Failed to clear governance cache at %s"), file_path)); } - node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.govman, *node.mn_sync, *node.netfulfilledman, *node.connman)); } + // Always register NetGovernance so it can suppress governance inv items in AlreadyHave() + // even when -disablegovernance is set. The handler's ProcessMessage/Schedule paths + // early-return on !IsValid(), and AlreadyHave() short-circuits to true so we don't + // track governance inventory that cannot be processed. + node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.govman, *node.mn_sync, *node.netfulfilledman, *node.connman)); node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.govman, *node.mn_sync, *node.connman, *node.netfulfilledman)); // ********************************************************* Step 8: start indexers diff --git a/src/llmq/dkgsessionmgr.cpp b/src/llmq/dkgsessionmgr.cpp index ea45374bda27..d87d8de39036 100644 --- a/src/llmq/dkgsessionmgr.cpp +++ b/src/llmq/dkgsessionmgr.cpp @@ -89,6 +89,9 @@ bool CheckDKGMessageStructure(std::string_view msg_type, const CDataStream& vRec if (msg_type == NetMsgType::QCONTRIB) { CDKGContribution qc; s >> qc; + // Contributions encrypt one blob per actual selected member. That count is + // between minSize and size (not necessarily equal to params.size), matching + // the later session-worker check against members.size(). return qc.vvec != nullptr && qc.vvec->size() == threshold && qc.contributions != nullptr && qc.contributions->blobs.size() >= min_size && diff --git a/src/llmq/signing_shares.cpp b/src/llmq/signing_shares.cpp index ea7a6f428c01..2a489f6d98df 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -388,6 +388,9 @@ void CSigSharesManager::ProcessMessage(const CNode& pfrom, const std::string& ms return; } } else if (msg_type == NetMsgType::QBSIGSHARES) { + // The inner sigShares vector is bounded by CBatchedSigShares's + // SERIALIZE_METHODS, but many individually-valid batches could still + // exceed the aggregate cap. Stop decoding as soon as either count does. std::vector msgs; try { msgs = UnserializeBatchedSigShares(vRecv); @@ -1805,9 +1808,16 @@ void CSigSharesManager::DispatchPendingProcessing() }); } - if (hasWork) { - // Work exists - spawn a worker to help! + static constexpr int MAX_UNVERIFIED_BATCHES{4}; + if (hasWork && unverifiedBatches < MAX_UNVERIFIED_BATCHES) { + // Work exists - spawn a bounded number of workers to help. The guard + // decrements the count on every exit path, including exceptions. + ++unverifiedBatches; workerPool.push([this](int) { + struct UnverifiedBatchGuard { + std::atomic& count; + ~UnverifiedBatchGuard() { --count; } + } guard{unverifiedBatches}; ProcessPendingSigSharesLoop(); }); } diff --git a/src/llmq/signing_shares.h b/src/llmq/signing_shares.h index 873f93384b42..b629e972fead 100644 --- a/src/llmq/signing_shares.h +++ b/src/llmq/signing_shares.h @@ -498,6 +498,7 @@ class CSigSharesManager : public llmq::CRecoveredSigsListener CleanupThrottler cleanupThrottler; std::atomic recoveredSigsCounter{0}; + std::atomic unverifiedBatches{0}; public: CSigSharesManager() = delete; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 8ec68002dbc0..89104a66ace7 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -146,6 +146,7 @@ static constexpr auto BLOCK_STALLING_TIMEOUT_MAX{64s}; static const int MAX_CMPCTBLOCK_DEPTH = 5; /** Maximum depth of blocks we're willing to respond to GETBLOCKTXN requests for. */ static const int MAX_BLOCKTXN_DEPTH = 10; +static_assert(MAX_BLOCKTXN_DEPTH <= MIN_BLOCKS_TO_KEEP, "MAX_BLOCKTXN_DEPTH too high"); /** Size of the "block download window": how far ahead of our current height do we fetch? * Larger windows tolerate larger download speed differences between peer, but increase the potential * degree of disordering of blocks on disk (which make reindexing and pruning harder). We'll probably @@ -2418,7 +2419,10 @@ bool PeerManagerImpl::AlreadyHave(const CInv& inv) case MSG_GOVERNANCE_OBJECT: case MSG_GOVERNANCE_OBJECT_VOTE: - return !m_govman.ConfirmInventoryRequest(inv); + for (const auto& handler : m_handlers) { + if (handler->AlreadyHave(inv)) return true; + } + return false; case MSG_QUORUM_FINAL_COMMITMENT: return m_llmq_ctx->quorum_block_processor->HasMineableCommitment(inv.hash); @@ -2745,47 +2749,66 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& } } - LOCK(cs_main); - const CBlockIndex* pindex = m_chainman.m_blockman.LookupBlockIndex(inv.hash); - if (!pindex) { - return; - } - if (!BlockRequestAllowed(pindex)) { - LogPrint(BCLog::NET, "%s: ignoring request from peer=%i for old block that isn't in the main chain\n", __func__, pfrom.GetId()); - return; - } const CNetMsgMaker msgMaker(pfrom.GetCommonVersion()); - // disconnect node in case we have reached the outbound limit for serving historical blocks - if (m_connman.OutboundTargetReached(true) && - (((m_chainman.m_best_header != nullptr) && (m_chainman.m_best_header->GetBlockTime() - pindex->GetBlockTime() > HISTORICAL_BLOCK_AGE)) || inv.IsMsgFilteredBlk()) && - !pfrom.HasPermission(NetPermissionFlags::Download) // nodes with the download permission may exceed target - ) { - LogPrint(BCLog::NET, "historical block serving limit reached, disconnect peer=%d\n", pfrom.GetId()); - pfrom.fDisconnect = true; - return; - } - // Avoid leaking prune-height by never sending blocks below the NODE_NETWORK_LIMITED threshold - if (!pfrom.HasPermission(NetPermissionFlags::NoBan) && ( - (((peer.m_our_services & NODE_NETWORK_LIMITED) == NODE_NETWORK_LIMITED) && ((peer.m_our_services & NODE_NETWORK) != NODE_NETWORK) && (m_chainman.ActiveChain().Tip()->nHeight - pindex->nHeight > (int)NODE_NETWORK_LIMITED_MIN_BLOCKS + 2 /* add two blocks buffer extension for possible races */) ) - )) { - LogPrint(BCLog::NET, "Ignore block request below NODE_NETWORK_LIMITED threshold, disconnect peer=%d\n", pfrom.GetId()); - //disconnect node and prevent it from stalling (would otherwise wait for the missing block) - pfrom.fDisconnect = true; - return; - } - // Pruned nodes may have deleted the block, so check whether - // it's available before trying to send. - if (!(pindex->nStatus & BLOCK_HAVE_DATA)) { - return; + + const CBlockIndex* pindex{nullptr}; + const CBlockIndex* tip{nullptr}; + bool can_direct_fetch{false}; + FlatFilePos block_pos{}; + { + LOCK(cs_main); + pindex = m_chainman.m_blockman.LookupBlockIndex(inv.hash); + if (!pindex) { + return; + } + if (!BlockRequestAllowed(pindex)) { + LogPrint(BCLog::NET, "%s: ignoring request from peer=%i for old block that isn't in the main chain\n", __func__, pfrom.GetId()); + return; + } + // disconnect node in case we have reached the outbound limit for serving historical blocks + if (m_connman.OutboundTargetReached(true) && + (((m_chainman.m_best_header != nullptr) && (m_chainman.m_best_header->GetBlockTime() - pindex->GetBlockTime() > HISTORICAL_BLOCK_AGE)) || inv.IsMsgFilteredBlk()) && + !pfrom.HasPermission(NetPermissionFlags::Download) // nodes with the download permission may exceed target + ) { + LogPrint(BCLog::NET, "historical block serving limit reached, disconnect peer=%d\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + tip = m_chainman.ActiveChain().Tip(); + // Avoid leaking prune-height by never sending blocks below the NODE_NETWORK_LIMITED threshold + if (!pfrom.HasPermission(NetPermissionFlags::NoBan) && ( + (((peer.m_our_services & NODE_NETWORK_LIMITED) == NODE_NETWORK_LIMITED) && ((peer.m_our_services & NODE_NETWORK) != NODE_NETWORK) && (tip->nHeight - pindex->nHeight > (int)NODE_NETWORK_LIMITED_MIN_BLOCKS + 2 /* add two blocks buffer extension for possible races */) ) + )) { + LogPrint(BCLog::NET, "Ignore block request below NODE_NETWORK_LIMITED threshold, disconnect peer=%d\n", pfrom.GetId()); + //disconnect node and prevent it from stalling (would otherwise wait for the missing block) + pfrom.fDisconnect = true; + return; + } + // Pruned nodes may have deleted the block, so check whether + // it's available before trying to send. + if (!(pindex->nStatus & BLOCK_HAVE_DATA)) { + return; + } + can_direct_fetch = CanDirectFetch(); + block_pos = pindex->GetBlockPos(); } + std::shared_ptr pblock; if (a_recent_block && a_recent_block->GetHash() == pindex->GetBlockHash()) { pblock = a_recent_block; } else { // Send block from disk std::shared_ptr pblockRead = std::make_shared(); - if (!ReadBlockFromDisk(*pblockRead, pindex, m_chainparams.GetConsensus())) - assert(!"cannot load block from disk"); + const bool read_ok = ReadBlockFromDisk(*pblockRead, block_pos, m_chainparams.GetConsensus()); + if (!read_ok || pblockRead->GetHash() != inv.hash) { + if (WITH_LOCK(cs_main, return m_chainman.m_blockman.IsBlockPruned(pindex))) { + LogPrint(BCLog::NET, "Block was pruned before it could be read, disconnect peer=%d\n", pfrom.GetId()); + } else { + LogError("Cannot load block from disk, disconnect peer=%d\n", pfrom.GetId()); + } + pfrom.fDisconnect = true; + return; + } pblock = pblockRead; } if (pblock) { @@ -2827,8 +2850,8 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& // they won't have a useful mempool to match against a compact block, // and we don't feel like constructing the object for them, so // instead we respond with the full, non-compact block. - if (CanDirectFetch() && - pindex->nHeight >= m_chainman.ActiveChain().Height() - MAX_CMPCTBLOCK_DEPTH) { + if (can_direct_fetch && + pindex->nHeight >= tip->nHeight - MAX_CMPCTBLOCK_DEPTH) { if (a_recent_compact_block && a_recent_compact_block->header.GetHash() == pindex->GetBlockHash()) { m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::CMPCTBLOCK, *a_recent_compact_block)); @@ -2850,7 +2873,7 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& // and we want it right after the last block so they don't // wait for other stuff first. std::vector vInv; - vInv.push_back(CInv(MSG_BLOCK, m_chainman.ActiveChain().Tip()->GetBlockHash())); + vInv.emplace_back(MSG_BLOCK, tip->GetBlockHash()); m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::INV, vInv)); peer.m_continuation_block.SetNull(); } @@ -4674,6 +4697,7 @@ void PeerManagerImpl::ProcessMessage( return; } + FlatFilePos block_pos{}; { LOCK(cs_main); @@ -4684,15 +4708,21 @@ void PeerManagerImpl::ProcessMessage( } if (pindex->nHeight >= m_chainman.ActiveChain().Height() - MAX_BLOCKTXN_DEPTH) { - CBlock block; - bool ret = ReadBlockFromDisk(block, pindex, m_chainparams.GetConsensus()); - assert(ret); - - SendBlockTransactions(pfrom, block, req); - return; + block_pos = pindex->GetBlockPos(); } } + if (!block_pos.IsNull()) { + CBlock block; + const bool read_ok = ReadBlockFromDisk(block, block_pos, m_chainparams.GetConsensus()); + // If height is above MAX_BLOCKTXN_DEPTH then this block cannot get + // pruned after we release cs_main above, so this read should never fail. + assert(read_ok && block.GetHash() == req.blockhash); + + SendBlockTransactions(pfrom, block, req); + return; + } + // If an older block is requested (should never happen in practice, // but can happen in tests) send a block response instead of a // blocktxn response. Sending a full block response instead of a diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index b657643d4101..673f25339506 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -517,7 +517,7 @@ void OverviewPage::coinJoinStatus(bool fForce) { if (!walletModel || !clientModel) return; - if (!fForce && (clientModel->node().shutdownRequested() || !clientModel->masternodeSync().isBlockchainSynced())) return; + if (!fForce && clientModel->node().shutdownRequested()) return; // Disable any PS UI for masternode or when autobackup is disabled or failed for whatever reason if (clientModel->node().isMasternode() || nWalletBackups <= 0) { @@ -542,6 +542,13 @@ void OverviewPage::coinJoinStatus(bool fForce) timer->start(1000); } + const QString strCoinJoinName{QString::fromStdString(gCoinJoinName)}; + bool notMixing{true}; + if (!walletModel->withCoinJoin([&](auto& client) { notMixing = !client.isMixing(); })) return; + ui->toggleCoinJoin->setText(notMixing ? tr("Start %1").arg(strCoinJoinName) : tr("Stop %1").arg(strCoinJoinName)); + + if (!fForce && !clientModel->masternodeSync().isBlockchainSynced()) return; + // Wrap all coinjoin related widgets we want to show/hide state based. // Value of the map contains a flag if this widget belongs to the advanced // CoinJoin UI option or not. True if it does, false if not. @@ -595,11 +602,10 @@ void OverviewPage::coinJoinStatus(bool fForce) } ui->labelCoinJoinEnabled->setToolTip(strKeysLeftText); - QString strCoinJoinName = QString::fromStdString(gCoinJoinName); - bool notMixing{false}; + // strCoinJoinName/notMixing are already resolved above, before the + // blockchain-sync gate, so the toggle label stays correct while syncing. bool refreshProgress{false}; walletModel->withCoinJoin([&](auto& client) { - notMixing = !client.isMixing(); if (notMixing && nBestHeight != client.getCachedBlocks()) { client.setCachedBlocks(nBestHeight); refreshProgress = true; @@ -608,7 +614,6 @@ void OverviewPage::coinJoinStatus(bool fForce) if (refreshProgress) updateCoinJoinProgress(); if (notMixing) { setWidgetsVisible(false); - ui->toggleCoinJoin->setText(tr("Start %1").arg(strCoinJoinName)); QString strEnabled = tr("Disabled"); // Show how many keys left in advanced PS UI mode only @@ -664,8 +669,8 @@ void OverviewPage::coinJoinStatus(bool fForce) } } - QString strEnabled; - walletModel->withCoinJoin([&](auto& client) { strEnabled = client.isMixing() ? tr("Enabled") : tr("Disabled"); }); + // Only reachable when notMixing is false (the branch above returns), so mixing is active here. + QString strEnabled{tr("Enabled")}; // Show how many keys left in advanced PS UI mode only if(fShowAdvancedCJUI && !strKeysLeftText.isEmpty()) strEnabled += ", " + strKeysLeftText; ui->labelCoinJoinEnabled->setText(strEnabled); @@ -752,9 +757,12 @@ void OverviewPage::toggleCoinJoin(){ ui->toggleCoinJoin->setText(tr("Start %1").arg(strCoinJoinName)); client.resetPool(); client.stopMixing(); - } else { + } else if (client.startMixing()) { ui->toggleCoinJoin->setText(tr("Stop %1").arg(strCoinJoinName)); - client.startMixing(); + } else { + // startMixing() can fail (e.g. locked wallet); keep the label in sync + // with the client's authoritative mixing state rather than assuming success. + ui->toggleCoinJoin->setText(tr("Start %1").arg(strCoinJoinName)); } }); } diff --git a/src/spork.cpp b/src/spork.cpp index 3dbe8db9ecb2..bd2cf276a629 100644 --- a/src/spork.cpp +++ b/src/spork.cpp @@ -127,7 +127,11 @@ void CSporkManager::CheckAndRemove() MessageProcessingResult CSporkManager::ProcessMessage(CNode& peer, CConnman& connman, std::string_view msg_type, CDataStream& vRecv) { if (msg_type == NetMsgType::SPORK) { - return ProcessSpork(peer.GetId(), vRecv); + try { + return ProcessSpork(peer.GetId(), vRecv); + } catch (const std::ios_base::failure& e) { + return MisbehavingError{100, strprintf("malformed spork received. peer=%d error=%s", peer.GetId(), e.what())}; + } } else if (msg_type == NetMsgType::GETSPORKS) { ProcessGetSporks(peer, connman); } diff --git a/src/wallet/test/coinjoin_tests.cpp b/src/wallet/test/coinjoin_tests.cpp index 7d10d4b768ce..b5bec624ad77 100644 --- a/src/wallet/test/coinjoin_tests.cpp +++ b/src/wallet/test/coinjoin_tests.cpp @@ -27,6 +27,13 @@ BOOST_FIXTURE_TEST_SUITE(coinjoin_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(coinjoin_options_tests) { + // Save and restore process-global CoinJoin options. ForceSetArg keys are + // stored without the leading '-', so ForceRemoveArg must match that form. + const bool was_enabled{CCoinJoinClientOptions::IsEnabled()}; + const bool was_multisession{CCoinJoinClientOptions::IsMultiSessionEnabled()}; + const int was_rounds{CCoinJoinClientOptions::GetRounds()}; + const int was_amount{CCoinJoinClientOptions::GetAmount()}; + gArgs.ForceSetArg("-enablecoinjoin", "0"); const auto loader{interfaces::MakeCoinJoinLoader(m_node)}; @@ -54,6 +61,12 @@ BOOST_AUTO_TEST_CASE(coinjoin_options_tests) BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetRounds(), DEFAULT_COINJOIN_ROUNDS + 10); CCoinJoinClientOptions::SetAmount(DEFAULT_COINJOIN_AMOUNT + 50); BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetAmount(), DEFAULT_COINJOIN_AMOUNT + 50); + + gArgs.ForceRemoveArg("enablecoinjoin"); + CCoinJoinClientOptions::SetEnabled(was_enabled); + CCoinJoinClientOptions::SetMultiSessionEnabled(was_multisession); + CCoinJoinClientOptions::SetRounds(was_rounds); + CCoinJoinClientOptions::SetAmount(was_amount); } BOOST_AUTO_TEST_CASE(coinjoin_collateral_tests) @@ -226,6 +239,8 @@ class CTransactionBuilderTestSetup : public TestChain100Setup BOOST_FIXTURE_TEST_CASE(coinjoin_manager_start_stop_tests, CTransactionBuilderTestSetup) { + // Ensure a known baseline: previous cases must not leave autostart enabled. + BOOST_CHECK_EQUAL(gArgs.GetBoolArg("-coinjoinautostart", DEFAULT_COINJOIN_AUTOSTART), DEFAULT_COINJOIN_AUTOSTART); BOOST_CHECK(m_node.cj_walletman->doForClient("", [](auto& cj_man) { BOOST_CHECK_EQUAL(cj_man.isMixing(), false); BOOST_CHECK_EQUAL(cj_man.startMixing(), true); @@ -236,6 +251,30 @@ BOOST_FIXTURE_TEST_CASE(coinjoin_manager_start_stop_tests, CTransactionBuilderTe })); } +BOOST_FIXTURE_TEST_CASE(coinjoin_duplicate_wallet_add_does_not_reinitialize_tests, CTransactionBuilderTestSetup) +{ + const bool was_enabled{CCoinJoinClientOptions::IsEnabled()}; + CCoinJoinClientOptions::SetEnabled(true); + gArgs.ForceSetArg("-coinjoinautostart", "1"); + + bool mixing{true}; + BOOST_CHECK(m_node.cj_walletman->doForClient("", [&](auto& cj_man) { + cj_man.stopMixing(); + mixing = cj_man.isMixing(); + })); + BOOST_CHECK_EQUAL(mixing, false); + + m_node.coinjoin_loader->AddWallet(wallet); + BOOST_CHECK(m_node.cj_walletman->doForClient("", [&](auto& cj_man) { mixing = cj_man.isMixing(); })); + + // ForceSetArg stores under SettingName() (no leading '-'); ForceRemoveArg + // must use the same key or the forced setting leaks into later fixtures and + // causes them to autostart mixing. + gArgs.ForceRemoveArg("coinjoinautostart"); + CCoinJoinClientOptions::SetEnabled(was_enabled); + BOOST_CHECK_EQUAL(mixing, false); +} + // End-to-end check that NewKeyPool() stops mixing BOOST_FIXTURE_TEST_CASE(coinjoin_newkeypool_stops_mixing_tests, CTransactionBuilderTestSetup) { diff --git a/test/functional/feature_llmq_simplepose.py b/test/functional/feature_llmq_simplepose.py index 274e71556cab..468f22f9afdc 100755 --- a/test/functional/feature_llmq_simplepose.py +++ b/test/functional/feature_llmq_simplepose.py @@ -141,13 +141,26 @@ def mine_quorum_less_checks(self, expected_good_nodes, mninfos_online): self.wait_for_quorum_phase(q, 6, expected_good_nodes, None, 0, mninfos_online) self.log.info("Waiting final commitment") - self.wait_for_quorum_commitment(q, mninfos_online) + # Only expect commitments from the good contributors. Deaf/probe-fail MNs + # remain in mninfos_online (so DKG still sees them) but often never publish + # a minable commitment; requiring every listed MN times out under close_mn_port. + got_commitment = self.wait_for_quorum_commitment( + q, mninfos_online, min_committing_mns=expected_good_nodes, do_assert=False) self.log.info("Mining final commitment") self.bump_mocktime(1, nodes=nodes) self.nodes[0].getblocktemplate() # this calls CreateNewBlock self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(nodes)) + if not got_commitment: + # No final commitment surfaced under contention; the round produced + # a null commitment block above. Advance out of the DKG round and + # signal failure so the caller can retry. + self.log.info("No final commitment observed; mined null commitment block to advance out of DKG round") + self.bump_mocktime(8) + self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes)) + return False + self.log.info("Waiting for quorum to appear in the list") self.wait_for_quorum_list(q, nodes) @@ -160,7 +173,7 @@ def mine_quorum_less_checks(self, expected_good_nodes, mninfos_online): self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes)) self.log.info("New quorum: height=%d, quorumHash=%s, quorumIndex=%d, minedBlock=%s" % (quorum_info["height"], new_quorum, quorum_info["quorumIndex"], quorum_info["minedBlock"])) - return new_quorum + return True def test_banning(self, invalidate_proc, expected_connections=None): mninfos_online = self.mninfo.copy() @@ -193,17 +206,48 @@ def test_banning(self, invalidate_proc, expected_connections=None): self.reset_probe_timeouts() self.mine_quorum(expected_connections=expected_connections, expected_members=expected_contributors, expected_contributions=expected_contributors, expected_complaints=expected_complaints, expected_commitments=expected_contributors, mninfos_online=mninfos_online, mninfos_valid=mninfos_valid) else: - # It's ok to miss probes/quorum connections up to 5 times. - # 6th time is when it should be banned for sure. + # close_mn_port keeps the deaf MN in mninfos_online so DKG still + # observes it. PoSe only advances when a mined final commitment + # excludes that MN from validMembers (HandleQuorumCommitment / + # PoSePunish(CalcPenalty(66))). Successful quorums do not always + # do so — parent counterexample: 6 successful commitments with + # only one punish (validMembers often still 5). Stopping after a + # fixed successful-round count leaves the MN unbanned; a wall- + # clock wait cannot invent missing penalty. + # + # Keep mining DKG rounds until the target is banned, with a hard + # attempt cap covering both null-DKG skips and non-punishing + # successful commitments. Two punishes (with per-block decay + # between 24-block DKG cycles) are typically enough to hit max. assert expected_connections is None - for j in range(6): - self.log.info(f"Accumulating PoSe penalty {j + 1}/6") + successful_rounds = 0 + attempts = 0 + max_attempts = 24 + while not check_banned(self.nodes[0], mn) and attempts < max_attempts: + attempts += 1 + self.log.info( + f"Accumulating PoSe penalty for {mn.proTxHash[:16]}... " + f"(successful_quorums={successful_rounds}, attempt {attempts}/{max_attempts})" + ) self.reset_probe_timeouts() - self.mine_quorum_less_checks(expected_contributors - 1, mninfos_online) - if check_banned(self.nodes[0], mn): - break - - assert check_banned(self.nodes[0], mn) + if self.mine_quorum_less_checks(expected_contributors - 1, mninfos_online): + successful_rounds += 1 + else: + self.log.info( + "Skipping null-DKG round (no final commitment); " + "not counted toward PoSe accumulation" + ) + if not check_banned(self.nodes[0], mn): + # Short RPC lag poll only — does not invent missing PoSe penalty. + self.wait_until(lambda: check_banned(self.nodes[0], mn), timeout=10, do_assert=False) + assert check_banned(self.nodes[0], mn), ( + f"MN {mn.proTxHash} not PoSe-banned after {successful_rounds} " + f"successful quorum rounds in {attempts} attempts" + ) + + # Ban state is updated during block validation and can lag RPC under load. + if not check_banned(self.nodes[0], mn): + self.wait_until(lambda: check_banned(self.nodes[0], mn), timeout=10) if not went_offline: # we do not include PoSe banned mns in quorums, so the next one should have 1 contributor less diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 301804a12da2..f49306a0225d 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -2085,14 +2085,25 @@ def check_dkg_session(): self.wait_until(check_dkg_session, timeout=timeout, sleep=sleep) - def wait_for_quorum_commitment(self, quorum_hash, mninfos, llmq_type=100, timeout=15): + def wait_for_quorum_commitment(self, quorum_hash, mninfos, llmq_type=100, timeout=30, min_committing_mns=None, do_assert=True): + # Wait until at least min_committing_mns of the listed MNs expose a non-null + # minable commitment (default: all of them, matching the historical behaviour). + # + # min_committing_mns counts *masternodes*, not DKG messages. Callers that keep + # deaf/probe-fail MNs in mninfos (close_mn_port) pass the number of good + # contributors, because those MNs stay visible to DKG phase waits but often never + # publish a minable commitment. Do not pass mine_quorum()'s expected_commitments + # here: that is a per-MN premature-commitment message count, a different unit. + if min_committing_mns is None: + min_committing_mns = len(mninfos) + def check_dkg_comitments(): + commitment_count = 0 for mn in mninfos: s = mn.get_node(self).quorum("dkgstatus") if "minableCommitments" not in s: - return False + continue commits = s["minableCommitments"] - c_ok = False for c in commits: if c["llmqType"] != llmq_type: continue @@ -2100,15 +2111,13 @@ def check_dkg_comitments(): continue if c["quorumPublicKey"] == '0' * 96: continue - c_ok = True + commitment_count += 1 break - if not c_ok: - return False - return True + return commitment_count >= min_committing_mns - self.wait_until(check_dkg_comitments, timeout=timeout) + return self.wait_until(check_dkg_comitments, timeout=timeout, do_assert=do_assert) - def wait_for_quorum_list(self, quorum_hash, nodes, timeout=15, llmq_type_name="llmq_test"): + def wait_for_quorum_list(self, quorum_hash, nodes, timeout=30, llmq_type_name="llmq_test"): def wait_func(): return quorum_hash in self.nodes[0].quorum('list')[llmq_type_name] self.log.info(f"quorums: {self.nodes[0].quorum('list')}") @@ -2189,6 +2198,10 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec self.wait_for_quorum_phase(q, 6, expected_members, None, 0, mninfos_online, llmq_type_name=llmq_type_name) self.log.info("Waiting final commitment") + # NOTE: deliberately not passing expected_commitments here. That parameter counts + # premature-commitment *messages* received per MN (phase 5), which is a different + # unit from the number of MNs exposing a minable commitment. mine_quorum() has no + # deaf/probe-fail MNs, so every listed MN must publish one. self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type) self.log.info("Mining final commitment")