From 73e72546f0ece7225041e3e94877e6df3e3d21a4 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 14 Oct 2026 15:47:28 -0500 Subject: [PATCH 1/4] Merge #7682: build: finish relic_conf.h regeneration before compiling BLS consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 24739989dd17321322480c91b09128e15f673a9d build: finish relic_conf.h regeneration before compiling BLS consumers (pasta) Pull request description: ## Issue being fixed or feature implemented A parallel build right after `configure` can fail intermittently with: ``` ./dashbls/include/dashbls/privatekey.hpp:18:10: fatal error: 'relic_conf.h' file not found ``` The header is not missing. `src/dashbls/configure.ac` finishes with ``` sed -i.old 's/PACKAGE/DASHBLS/g' depends/relic/include/relic_conf.h sed -i.old 's/PACKAGE/DASHBLS/g' config.status ``` The second `sed` runs after `stamp-h1` is written, so it leaves `config.status` newer than the header's stamp. The first `make` in `src/dashbls` then reruns `config.status`. That run rewrites `relic_conf.h` with `rm -f` followed by `mv`. Meanwhile, `src` compiles objects that include dashbls headers in parallel. Any object that opens the header between the `rm` and the `mv` fails. (The regenerated header also keeps the `PACKAGE_*` defines that the first `sed` removed. That is the conflict reported in #5270. This PR only fixes the ordering.) ## What was done? Added `relic_conf.h` to `BUILT_SOURCES` in `src/Makefile.am`, with a `FORCE` rule that runs `make -C dashbls depends/relic/include/relic_conf.h`. Automake builds `BUILT_SOURCES` before `all`, `check`, and `install` recurse. Any pending regeneration therefore finishes before anything in `src` is compiled. When the header is already current, the sub-make is a no-op. The first version of this PR had a rule with no prerequisites plus a hand-maintained list of object variables. That rule never ran, because the header already exists after `configure`. The object list also missed some consumers, such as `libbitcoin_ipc_a`. It has been replaced by the change above. ## How Has This Been Tested? The race window is short, so I widened it: I added `sleep 20` to the dashbls `config.status` between the `rm` and the `mv`. I then ran `make clean`, restored the header to its post-`configure` content, touched `config.status` to match the post-`configure` state, and ran `make -j16 -k` (macOS arm64, depends build): | | result | | --- | --- | | `develop` | exit 2, 102 × `'relic_conf.h' file not found` | | this PR | exit 0, no errors | Also checked without fault injection: - A clean `configure` followed by `make -j16` succeeds. - A second `make` recompiles nothing, so `FORCE` does not trigger rebuilds. ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone (for repository code-owners and collaborators only) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: knst: utACK 24739989dd17321322480c91b09128e15f673a9d [see comment] Tree-SHA512: 4f806dec85f0c36b5aab8e8e68120a73f3d5c777ede7198914b60bd88c854dced91a27270fd0498776075f6e96789e81e4af7543cf1bf263275309368acf3e34 (cherry picked from commit 8246da112d5227d0a01b5745ded3332df429f1be) --- src/Makefile.am | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Makefile.am b/src/Makefile.am index 44fb7be39ced..add830dc7a55 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -31,6 +31,14 @@ check_PROGRAMS = TESTS = BENCHMARKS = +# The first make in dashbls after configure reruns its config.status, which +# replaces relic_conf.h. Let that finish before compiling anything that +# includes it. +BUILT_SOURCES = dashbls/depends/relic/include/relic_conf.h + +dashbls/depends/relic/include/relic_conf.h: FORCE + $(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C dashbls depends/relic/include/relic_conf.h + if ENABLE_STACKTRACES if ENABLE_CRASH_HOOKS if CRASH_HOOKS_WRAPPED_CXX_ABI From 9cd1c722cf49a59931178573a4bb922ddcdf29a1 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 15 Oct 2026 21:50:39 -0500 Subject: [PATCH 2/4] Merge #7869: fix: let startup block replay handle quorum commitments and asset unlocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 74dc3e5a41f5ab6ce0c12e81f723961b37eaaa6a test: replay blocks with quorum commitments and asset unlocks after a crash (pasta) f0578174ff82f27282139f2c7a02c76272399649 fix: replay blocks under the BLS scheme they were connected with (pasta) 4585beeeeade7414af47c07512a4071e5310a337 fix: give replayed blocks the chain view they were connected with (pasta) Pull request description: ## Issue being fixed or feature implemented When a node stops in the middle of a coins-database flush (crash, power loss, OOM kill), the next start rolls the unflushed blocks forward in `ReplayBlocks()`. That runs inside `LoadChainstate()` before `LoadChainTip()`, so the chain is still empty while `RollforwardBlock()` re-processes each block's special transactions and quorum commitments. Several of those checks read the chain, and two things go wrong on a chain with quorums: - A block containing a mined quorum commitment aborts the node: `IsMiningPhase()` asserts `nHeight <= active_chain.Height() + 1`, and the chain height is -1. The existing "skip when replaying" branches in `CQuorumBlockProcessor` do not cover that assertion, so the node crashes again on every start until `-reindex-chainstate`. - Asset-unlock and MNHF-signal transactions resolve their quorum against the chain, and with an empty chain they find none. Replay rejects the block (`bad-assetunlock-too-old-quorum`) and startup fails with "Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate." Separately, `RollforwardBlock()` processes blocks under whatever BLS scheme the process-wide flag holds. At that point in startup it holds the legacy default, because `VerifyLoadedChainstate()` only sets it later. So the first replayed post-V19 block runs under the legacy scheme, and an asset unlock in that block fails signature verification (`bad-assetunlock-not-verified`) even with a chain in place. `ConnectBlock()` already sets the scheme for each block with `ScopedBLSLegacyScheme`. The replay path never got that guard. The replay range covers every block since the previous coins flush, often hours of blocks. Mainnet mines a non-null quorum commitment roughly every half hour, so in practice this recovery always fails on mainnet and needs a full `-reindex-chainstate`. That costs masternodes hours offline. ## What was done? - `ReplayBlocks()` sets the chain tip to the parent of each block before rolling it forward. Each replayed block then sees the same chain view `ConnectBlock()` saw when the block was first connected. The tip is moved to the parent before each block, not to the block after it, so the chain ends one block short of the new coins tip. `LoadChainTip()` then still runs its full path: it sets the tip and prunes the block-index candidates that `LoadBlockIndex()` added while the chain was empty. - `RollforwardBlock()` establishes the block's BLS scheme the same way `ConnectBlock()` does. The guard is not committed: every replayed block sets its own scheme on entry, and `VerifyLoadedChainstate()` sets the scheme for the loaded tip afterwards. Both changes only affect local crash recovery of blocks this node already fully validated. Block validation rules, serialization and the P2P protocol are unchanged. The CbTx best-ChainLock check is not involved: replay passes `fCheckCbTxMerkleRoots=false`, so the coinbase payload checks are skipped there. The `chainstate.m_chain.Tip() == nullptr` "skip when replaying" branches in `CQuorumBlockProcessor` are no longer reached during replay, because replay now runs with the same chain view as `ConnectBlock()`. They are left in place to keep this change small and can be removed in a follow-up. ## How Has This Been Tested? New step `test_replay_after_crash` at the end of `feature_asset_locks.py`, which already has the platform quorum, evo masternodes and asset unlock helpers. It crashes the regular node mid-flush twice and checks the replay each time. Each cycle restarts the node with `-dbcrashratio=1 -dbbatchsize=1`, mines without flushing, and forces a flush (`gettxoutsetinfo`) that crashes the node after its first partial batch. The node is then restarted without peers and must log `Rolling forward` for every expected block, come back at the same tip, and have the same UTXO set hash as the other node. - Cycle 1 mines a new platform quorum commitment and then an asset unlock (index 900) signed by a platform quorum. It also checks the new quorum is listed after the restart. The commitment block is replayed before the unlock. - Cycle 2 starts from a clean flush with the quorum already mature and mines only an asset unlock (index 901), so the unlock is the first and only block rolled forward. No earlier replayed block resets the BLS scheme, so this cycle is what independently covers the `RollforwardBlock()` scheme guard. After the second cycle the node is restarted normally and reconnected, so the step leaves the file as it found it. Results (macOS arm64, `--enable-debug`, full `feature_asset_locks.py` run, about 3 minutes): - With this PR: passes, with and without `--usecli`. - With the two `src/validation.cpp` fixes reverted (the PR base): cycle 1 fails at the restart, with the node aborting during startup on the `IsMiningPhase()` assertion (`Assertion failed: (nHeight <= active_chain.Height() + 1), function IsMiningPhase, file blockprocessor.cpp`). The commitment block is replayed before the asset unlock block, so the test reaches that crash first. - With only the chain-tip change reverted (BLS guard kept): cycle 1 fails with the same `IsMiningPhase()` assertion. - With only the BLS scheme guard removed (chain-tip change kept): cycle 1 still passes, because the post-V19 blocks replayed before the unlock reset the global scheme as a side effect. Cycle 2 fails: startup aborts with "Unable to replay blocks" and the log shows `RollforwardBlock(DASH): ProcessSpecialTxsInBlock for block ... failed with bad-assetunlock-not-verified`. Also ran `feature_dbcrash.py` and the unit suites `validation_chainstatemanager_tests`, `validation_chainstate_tests`, `validation_flush_tests`, `evo_cbtx_tests`, `evo_snapshot_tests`, plus `test/lint/lint-whitespace.py`. ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 2a281674e6d8e48ec66f276ba7cb65869dc54c83fa1580214fb2441e275716de75a0bb564481e3e8db476c2114e9095c7f5be9548032955add476bd7c9279a02 (cherry picked from commit 39b5bbc1dd5b470414fea0e287fcd4424156cb79) v24.0.x adaptation: develop folded CChainstateHelper into Chainstate (d5c1bd724e), which is not on this branch, so RollforwardBlock() keeps calling m_chain_helper->special_tx instead of m_special_tx. --- src/validation.cpp | 8 +++++ test/functional/feature_asset_locks.py | 47 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/validation.cpp b/src/validation.cpp index 85d41ae6d7c6..0b86bd4b1293 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5200,6 +5200,9 @@ bool Chainstate::RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& in BlockValidationState state; const CAmount blockSubsidy = GetBlockSubsidy(pindex, m_chainman.GetConsensus()); MNListUpdates mnlist_updates; + // Use the scheme ConnectBlock used for this block instead of whatever replay inherited + ScopedBLSLegacyScheme bls_scheme_guard{ + !DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, m_chain, block, pindex, GetSpecialTxRules(pindex->pprev, m_chainman), inputs, blockSubsidy, /*fJustCheck=*/false, @@ -5298,6 +5301,11 @@ bool Chainstate::ReplayBlocks() LogPrintf("Rolling forward %s (%i)\n", pindex.GetBlockHash().ToString(), nHeight); uiInterface.ShowProgress(_("Replaying blocks…").translated, (int) ((nHeight - nForkHeight) * 100.0 / (pindexNew->nHeight - nForkHeight)) , false); + // Special transaction and quorum commitment checks read the chain, which + // LoadChainTip() has not set yet; give them the view ConnectBlock had. + // Stopping at the parent also keeps LoadChainTip() from skipping its + // candidate pruning. + m_chain.SetTip(*Assert(pindex.pprev)); if (!RollforwardBlock(&pindex, cache)) return false; } diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index 56536643ba39..493a0c74fdf5 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -5,7 +5,9 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. import copy +import http.client import struct +import subprocess from decimal import Decimal from io import BytesIO @@ -46,6 +48,7 @@ assert_equal, assert_greater_than, assert_greater_than_or_equal, + assert_raises, assert_raises_rpc_error, softfork_active, ) @@ -354,6 +357,7 @@ def run_test(self): self.test_v24_fork(node_wallet, node, pubkey) self.test_asset_unlock_v2(node_wallet, node, pubkey) self.test_non_standard(node_wallet, node, pubkey) + self.test_replay_after_crash(node_wallet, node, pubkey) def test_asset_locks(self, node_wallet, node, pubkey): @@ -1438,6 +1442,49 @@ def test_non_standard(self, node_wallet, node, pubkey): self.restart_node(1, self.extra_args[1]) self.connect_nodes(1, 0) + def test_replay_after_crash(self, node_wallet, node, pubkey): + self.log.info("Testing recovery from a crash mid-flush with a quorum commitment and an asset unlock to replay...") + # Every flush now dies after its first partial write, so nothing mined from here on is flushed + self.restart_with_crashing_flush() + + self.mine_quorum_2_nodes() + quorum_hash = node_wallet.quorum("list", 1)["llmq_test_platform"][0] + commitment_block = node_wallet.quorum("info", llmq_type_test, quorum_hash)["minedBlock"] + txid = self.send_tx(self.create_assetunlock(900, COIN, pubkey)) + unlock_block = self.generate(node_wallet, 1)[0] + assert txid in node_wallet.getblock(unlock_block)["tx"] + + self.log.info("Restart without peers: the commitment block is replayed before the asset unlock block") + self.crash_and_replay(node_wallet, node, [commitment_block, unlock_block]) + assert quorum_hash in node_wallet.quorum("list")["llmq_test_platform"] + + self.log.info("Replay an asset unlock as the first block rolled forward, with the quorum already mature and flushed") + self.restart_node(0, self.extra_args[0]) + self.restart_with_crashing_flush() + txid = self.send_tx(self.create_assetunlock(901, COIN, pubkey)) + unlock_block = self.generate(node_wallet, 1)[0] + assert txid in node_wallet.getblock(unlock_block)["tx"] + self.crash_and_replay(node_wallet, node, [unlock_block]) + + self.restart_node(0, self.extra_args[0]) + for i in range(1, len(self.nodes)): + self.connect_nodes(0, i) + + def restart_with_crashing_flush(self): + self.restart_node(0, self.extra_args[0] + ["-dbcrashratio=1", "-dbbatchsize=1"]) + for i in range(1, len(self.nodes)): + self.connect_nodes(0, i) + + def crash_and_replay(self, node_wallet, node, replayed_blocks): + tip = node_wallet.getbestblockhash() + self.log.info("Force a flush, which crashes the node") + assert_raises((ConnectionError, http.client.CannotSendRequest, subprocess.CalledProcessError), node_wallet.gettxoutsetinfo) + self.wait_for_node_exit(0, timeout=10) + with node_wallet.assert_debug_log([f"Rolling forward {block}" for block in replayed_blocks]): + self.start_node(0, extra_args=self.extra_args[0] + ["-connect=0"]) + assert_equal(node_wallet.getbestblockhash(), tip) + assert_equal(node_wallet.gettxoutsetinfo()["hash_serialized_2"], node.gettxoutsetinfo()["hash_serialized_2"]) + if __name__ == '__main__': AssetLocksTest().main() From 696d45fad536a3a8c98a298e866faa2acc9c1833 Mon Sep 17 00:00:00 2001 From: merge-script Date: Sun, 18 Oct 2026 10:15:42 +0700 Subject: [PATCH 3/4] Merge dashpay/dash#7861: feat: keep at least 4-days worth blocks after pruning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e828c5c3a3de984b91814cc0fa92a657966b4ff0 test: check pruned nodes keep the credit-pool rebuild history (Konstantin Akimov) e2ec035898f3fc5ae8f4e34f9865f443ea04b6a2 feat: keep four days of blocks on pruned nodes (Konstantin Akimov) 927acde464dd973284614e26d7db9a865ed3baba refactor: make the pruning retention a chain parameter (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented A pruned node can permanently reject a canonical block after a restart. To rebuild a credit pool, the node starts at the latest credit-pool disk snapshot (one every 576 blocks). For each block after it, it reads that block and the block one withdrawal window (CreditPoolPeriodBlocks, 576 on mainnet/testnet) earlier. The oldest block needed can be about two windows below the tip, but pruning only guarantees 288 blocks. Nothing protected those blocks from pruning. Rebuilding the members of a rotated LLMQ_60_75 cycle (GetHashModifierFromWorkBlock() → GetNonNullCoinbaseChainlock()) reads work blocks up to 6 × 288 + 50 + 8 = 1786 blocks below the tip. If one of them has been pruned, the modifier falls back to the block hash, and the node silently computes different members than the rest of the network. Beside that, bitcoin core has at least 288 blocks which equals to 2 days of blocks. While dash core doesn't change this constant and it equals to just 12 hours of blocks. ## What was done? Amount of blocks that kept after pruning is bumped to at least 576 * 4 blocks [4 days worth]. ## How Has This Been Tested? See updated unit & functional tests ## Breaking Changes N/A Disk storage may be slightly increased for nodes that uses pruning, though nothing extreme. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ Top commit has no ACKs. Tree-SHA512: 2f17a4984b7832d7f6c108e4710136aa5a3364236a6d1ff1960692fb7fc3b95599c73d370c00f31103f25d07fbcc8edcd7bffefc0b3df64ffb5b5ec6cfa193c8 (cherry picked from commit b3310fe03a1bf3a7ba73a8412af9ee68f498089e) v24.0.x adaptation: chain parameters live in src/chainparams.{h,cpp} on this branch (develop moved them to src/kernel/), and regtest's nPruneAfterHeight still reads -fastprune from ArgsManager here. --- doc/release-notes-7861.md | 12 ++++++++++++ src/chainparams.cpp | 8 ++++++-- src/chainparams.h | 7 +++++++ src/evo/creditpool.h | 3 +-- src/init.cpp | 4 ++-- src/net_processing.cpp | 4 ++-- src/node/blockstorage.cpp | 9 +++++---- src/node/blockstorage.h | 2 +- src/rpc/blockchain.cpp | 4 ++-- src/test/evo_assetlocks_tests.cpp | 12 ++++++++++++ src/validation.cpp | 2 +- src/validation.h | 5 +++-- 12 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 doc/release-notes-7861.md diff --git a/doc/release-notes-7861.md b/doc/release-notes-7861.md new file mode 100644 index 000000000000..cfa126788d91 --- /dev/null +++ b/doc/release-notes-7861.md @@ -0,0 +1,12 @@ +Pruning +------- + +- Pruned nodes on mainnet, testnet and devnets now keep at least four days of + blocks (2304) below the tip instead of 288. Validation reads older blocks + from disk: the credit pool is rebuilt from its newest daily snapshot after a + restart, and rotated quorum membership reads the work blocks of earlier DKG + cycles. `pruneblockchain` clamps its height accordingly, and on testnet and + devnets pruning starts once the chain is longer than 3000 blocks + (previously 1000). Blocks that an earlier version already pruned are not + recovered; a node that fails to validate a block after upgrading needs + `-reindex`. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index c46bc0a738b8..82b68c08d547 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -243,6 +243,7 @@ class CMainParams : public CChainParams { nDefaultPlatformP2PPort = 26656; nDefaultPlatformHTTPPort = 443; nPruneAfterHeight = 100000; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 57; m_assumed_chain_state_size = 1; @@ -446,7 +447,8 @@ class CTestNetParams : public CChainParams { nDefaultPort = 19999; nDefaultPlatformP2PPort = 22000; nDefaultPlatformHTTPPort = 22001; - nPruneAfterHeight = 1000; + nPruneAfterHeight = 3000; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 10; m_assumed_chain_state_size = 1; @@ -628,7 +630,8 @@ class CDevNetParams : public CChainParams { nDefaultPort = 19799; nDefaultPlatformP2PPort = 22100; nDefaultPlatformHTTPPort = 22101; - nPruneAfterHeight = 1000; + nPruneAfterHeight = 3000; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; @@ -873,6 +876,7 @@ class CRegTestParams : public CChainParams { nDefaultPlatformP2PPort = 22200; nDefaultPlatformHTTPPort = 22201; nPruneAfterHeight = args.GetBoolArg("-fastprune", false) ? 100 : 1000; + m_min_blocks_to_keep = 288; m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; diff --git a/src/chainparams.h b/src/chainparams.h index dc21b75f60f9..22e86ef13169 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -114,6 +114,8 @@ class CChainParams /** If this chain is exclusively used for testing */ bool IsTestChain() const { return m_is_test_chain; } uint64_t PruneAfterHeight() const { return nPruneAfterHeight; } + /** Block files containing a block-height within MinBlocksToKeep() of ActiveChain().Tip() will not be pruned. */ + int MinBlocksToKeep() const { return m_min_blocks_to_keep; } /** Minimum free space (in GB) needed for data directory */ uint64_t AssumedBlockchainSize() const { return m_assumed_blockchain_size; } /** Minimum free space (in GB) needed for data directory when pruned; Does not include prune target*/ @@ -166,6 +168,11 @@ class CChainParams CMessageHeader::MessageStartChars pchMessageStart; uint16_t nDefaultPort; uint64_t nPruneAfterHeight; + /// Four days of blocks (2304) on every network but regtest. Validation reads older blocks from disk: + /// a credit pool is rebuilt from its newest daily snapshot, reading the block one withdrawal window + /// before each block since it, and rotated LLMQ modifiers read the work block of earlier cycles. + /// Regtest keeps Bitcoin Core's 288 for the upstream pruning tests. + int m_min_blocks_to_keep{288}; uint64_t m_assumed_blockchain_size; uint64_t m_assumed_chain_state_size; std::vector vSeeds; diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index e7676beace54..060a570e17ca 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -123,9 +123,8 @@ class CCreditPoolManager CEvoDB& evoDb; const ChainstateManager& m_chainman; - static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day - public: + static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day static constexpr CAmount LimitAmountLow = 100 * COIN; static constexpr CAmount LimitAmountHigh = 1000 * COIN; static constexpr CAmount LimitAmountV22 = 2000 * COIN; diff --git a/src/init.cpp b/src/init.cpp index aa4d77b639ed..170936cbf62f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2099,9 +2099,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } if (status == node::ChainstateLoadStatus::SUCCESS) { uiInterface.InitMessage(_("Verifying blocks…").translated); - if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) { + if (chainman.m_blockman.m_have_pruned && options.check_blocks > chainman.GetParams().MinBlocksToKeep()) { LogWarning("pruned datadir may not have more than %d blocks; only checking available blocks\n", - MIN_BLOCKS_TO_KEEP); + chainman.GetParams().MinBlocksToKeep()); } std::tie(status, error) = catch_exceptions([&]{ return VerifyLoadedChainstate(chainman, options, *Assert(node.evodb), [](bool bls_state) { LogPrintf("AppInitMain: bls_legacy_scheme=%d\n", bls_state); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index c41876bc606c..d315c6f2a2cb 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -160,7 +160,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"); +static_assert(MAX_BLOCKTXN_DEPTH <= BLOCKS_NEAR_TIP, "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 @@ -4691,7 +4691,7 @@ void PeerManagerImpl::ProcessMessage( } // If pruning, don't inv blocks unless we have on disk and are likely to still have // for some reasonable time window (1 hour) that block relay might require. - const int nPrunedBlocksLikelyToHave = MIN_BLOCKS_TO_KEEP - 3600 / m_chainparams.GetConsensus().nPowTargetSpacing; + const int nPrunedBlocksLikelyToHave = BLOCKS_NEAR_TIP - 3600 / m_chainparams.GetConsensus().nPowTargetSpacing; if (m_chainman.m_blockman.IsPruneMode() && (!(pindex->nStatus & BLOCK_HAVE_DATA) || pindex->nHeight <= m_chainman.ActiveChain().Tip()->nHeight - nPrunedBlocksLikelyToHave)) { LogPrint(BCLog::NET, " getblocks stopping, pruned or too old block at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString()); break; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 1f2680fa0a6c..249a6dc2e259 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -171,8 +171,9 @@ void BlockManager::FindFilesToPruneManual(std::set& setFilesToPrune, int nM return; } - // last block to prune is the lesser of (user-specified height, MIN_BLOCKS_TO_KEEP from the tip) - unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, chain_tip_height - MIN_BLOCKS_TO_KEEP); + // last block to prune is the lesser of (user-specified height, MinBlocksToKeep() from the tip) + unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, + static_cast(chain_tip_height - GetParams().MinBlocksToKeep())); int count = 0; for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { if (m_blockfile_info[fileNumber].nSize == 0 || m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { @@ -195,7 +196,7 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr return; } - unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - static_cast(MIN_BLOCKS_TO_KEEP))}; + unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - GetParams().MinBlocksToKeep())}; uint64_t nCurrentUsage = CalculateCurrentUsage(); // We don't check to prune until after we've allocated new space for files // So we should leave a buffer under our target to account for another allocation @@ -225,7 +226,7 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr break; } - // don't prune files that could have a block within MIN_BLOCKS_TO_KEEP of the main chain's tip but keep scanning + // don't prune files that could have a block within MinBlocksToKeep() of the main chain's tip but keep scanning if (m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { continue; } diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 70f1547fb1c4..ec7ba9c76e81 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -112,7 +112,7 @@ class BlockManager * Pruning functions are called from FlushStateToDisk when the m_check_for_pruning flag has been set. * Block and undo files are deleted in lock-step (when blk00003.dat is deleted, so is rev00003.dat.) * Pruning cannot take place until the longest chain is at least a certain length (CChainParams::nPruneAfterHeight). - * Pruning will never delete a block within a defined distance (currently 288) from the active chain's tip. + * Pruning will never delete a block within CChainParams::MinBlocksToKeep() of the active chain's tip. * The block index is updated by unsetting HAVE_DATA and HAVE_UNDO for any blocks that were stored in the deleted files. * A db flag records the fact that at least some block files have been pruned. * diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 3b82e308a89d..4f98fa02fca0 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1120,9 +1120,9 @@ static RPCHelpMan pruneblockchain() throw JSONRPCError(RPC_MISC_ERROR, "Blockchain is too short for pruning."); } else if (height > chainHeight) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Blockchain is shorter than the attempted prune height."); - } else if (height > chainHeight - MIN_BLOCKS_TO_KEEP) { + } else if (height > chainHeight - chainman.GetParams().MinBlocksToKeep()) { LogPrint(BCLog::RPC, "Attempt to prune blocks close to the tip. Retaining the minimum number of blocks.\n"); - height = chainHeight - MIN_BLOCKS_TO_KEEP; + height = chainHeight - chainman.GetParams().MinBlocksToKeep(); } PruneBlockFilesManual(active_chainstate, height); diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp index 7bc4c09a5e85..b9780d3e4866 100644 --- a/src/test/evo_assetlocks_tests.cpp +++ b/src/test/evo_assetlocks_tests.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -858,6 +859,17 @@ BOOST_FIXTURE_TEST_CASE(credit_pool_snapshot_persisted_after_transactionless_con BOOST_CHECK_EQUAL(snapshot.currentLimit, pool.currentLimit); } +BOOST_FIXTURE_TEST_CASE(min_blocks_to_keep_covers_credit_pool_rebuild, BasicTestingSetup) +{ + // A pool is rebuilt from a snapshot up to one period old, reading the block one window before + // each block since it. Regtest deliberately keeps Bitcoin Core's 288 and is not checked. + for (const auto& chain : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::DEVNET}) { + const auto params = CreateChainParams(*m_node.args, chain); + BOOST_CHECK_GE(params->MinBlocksToKeep(), + CCreditPoolManager::DISK_SNAPSHOT_PERIOD + params->CreditPoolPeriodBlocks()); + } +} + BOOST_AUTO_TEST_CASE(credit_pool_unlock_limit_v24) { // From v24 the limit is relative and net: 20% of the balance at the window start, never less diff --git a/src/validation.cpp b/src/validation.cpp index 0b86bd4b1293..e20e6e9b824f 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4797,7 +4797,7 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, // blocks which are too close in height to the tip. Apply this test // regardless of whether pruning is enabled; it should generally be safe to // not process unrequested blocks. - bool fTooFarAhead{pindex->nHeight > ActiveHeight() + int(MIN_BLOCKS_TO_KEEP)}; + bool fTooFarAhead{pindex->nHeight > ActiveHeight() + int(BLOCKS_NEAR_TIP)}; // TODO: Decouple this function from the block download logic by removing fRequested // This requires some new chain data structure to efficiently look up if a diff --git a/src/validation.h b/src/validation.h index 2b6e8e6ce9f9..de5c2fc754e8 100644 --- a/src/validation.h +++ b/src/validation.h @@ -84,8 +84,9 @@ static const unsigned int MAX_HEADERS_COMPRESSED_RESULT = 8000; /** Default for -stopatheight */ static const int DEFAULT_STOPATHEIGHT = 0; -/** Block files containing a block-height within MIN_BLOCKS_TO_KEEP of ActiveChain().Tip() will not be pruned. */ -static const unsigned int MIN_BLOCKS_TO_KEEP = 288; +/** Blocks within this distance of ActiveChain().Tip() are accepted unrequested and announced by pruned nodes. + * Bitcoin Core's MIN_BLOCKS_TO_KEEP; pruning retention is CChainParams::MinBlocksToKeep(). */ +static const unsigned int BLOCKS_NEAR_TIP = 288; static const signed int DEFAULT_CHECKBLOCKS = 6; static constexpr int DEFAULT_CHECKLEVEL{3}; From 1ba2609ebd33c172bfe43452a287cc7aea4da2de Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 10:03:04 -0500 Subject: [PATCH 4/4] Merge #7951: fix(net): stop peers lingering after disconnect under edge-triggered epoll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cff6a7fce71197fe476b15ac3062693d5a1e40a3 fix(net): keep draining a socket until EAGAIN so edge-triggered epoll sees EOF (pasta) Pull request description: ## Issue being fixed or feature implemented On Linux, a peer that sends its last bytes and closes the connection in one go can stay connected after it has gone. This is what makes `p2p_addr_relay.py` flake in `linux64_multiprocess-test`: one of the test's `disconnect_p2ps()` calls times out because the node never drops one peer. It fails on develop's own CI too, e.g. [this run on 35a115ee7b](https://github.com/dashpay/dash/actions/runs/40806631960/job/123236402175), where it failed all 3 retries. In that log, the peer that stayed connected had its last `pong` read in the same millisecond the test closed its sockets, and it never logged `socket closed`. The cause: with `-socketevents=epoll` (the Linux default), sockets are registered `EPOLLET`. `CConnman::SocketRecvData` clears `fHasRecvData` after any short read. When the data and the FIN arrive in one readiness edge, the data is read, the flag is cleared, and epoll raises no further edge for the pending EOF. So `recv() == 0` never runs, and the peer lingers until eviction or shutdown. kqueue registers its read filter level-triggered, so macOS never hits this. ## What was done? `fHasRecvData` is now cleared only when `recv()` reports `EWOULDBLOCK`, meaning the socket is drained. After a short read the next socket-handler iteration calls `recv()` again, sees the EOF and disconnects the peer. In the level-triggered `poll`/`select` modes this costs one extra non-blocking `recv()` per drain. A receive-paused node that keeps the flag set does not cause a zero-timeout spin, because `HasUnpausedReceivableNode()` already excludes paused nodes. ## How Has This Been Tested? `--enable-debug` build on Linux x86_64, develop 35a115ee7b, with and without the patch: - **Real test under load:** 64 runs of `p2p_addr_relay.py` at once, 16 in parallel, pinned to 4 CPUs (`taskset -c 0-3`) to mimic a busy CI runner, each with its own `--portseed`. **Without the fix, 17/64 failed:** 9 at line 417 (the CI failure), the rest at the test's other `disconnect_p2ps()` calls (lines 308, 327, 432) plus one timed-out log match. **With the fix: 64/64 passed.** On an idle 28-core machine develop passes 64/64, which is why it looks like a flake. - **Deterministic reproducer (not committed):** a raw inbound socket sets `TCP_CORK`, sends a valid v1 `version` and then calls `shutdown(SHUT_WR)`, so the payload and the FIN arrive in one segment. 20 trials per mode, counting peers still connected after 5 s: | | epoll, corked | epoll, uncorked | poll, corked | poll, uncorked | |---|---|---|---|---| | develop | **20/20 lingered** | 2/20 | 0/20 | 0/20 | | this PR | 0/20 | 0/20 | 0/20 | 0/20 | I kept the PR to the one-line fix. I can add the reproducer as a functional test if reviewers want one. ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: knst: tACK cff6a7fce71197fe476b15ac3062693d5a1e40a3 Tree-SHA512: 8b8dc513e82e0ff4d205f77e45f36fe4e4922b9f5b687024402541147140b78e5f6f8027c94275a0d9d9aaab68a39ba8162184641acf56692fdb11ce2056d2a8 (cherry picked from commit faca2cc19be945f1436115c9977b970784e9d584) --- src/net.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net.cpp b/src/net.cpp index 4e9d3457cb7f..a8bb48cdc6d0 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2599,7 +2599,7 @@ size_t CConnman::SocketRecvData(CNode *pnode) if (!pnode->m_sock) return 0; nBytes = recv(pnode->m_sock->Get(), (char*)pchBuf, sizeof(pchBuf), MSG_DONTWAIT); - if (nBytes < (int)sizeof(pchBuf)) { + if (nBytes < 0 && WSAGetLastError() == WSAEWOULDBLOCK) { pnode->fHasRecvData = false; } }