Repository navigation
fix: bound mnListsCache admission to stop getmnlistd memory DoS - #62
Draft
PastaPastaPasta wants to merge 25 commits into
Draft
PastaPastaPasta wants to merge 25 commits into
PastaPastaPasta wants to merge 25 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PastaPastaPasta
force-pushed
the
sec/v044
branch
from
August 1, 2026 18:25
bf91603 to
f2f1e9f
Compare
PastaPastaPasta
force-pushed
the
sec/v044
branch
3 times, most recently
from
August 9, 2026 01:24
aa93a4c to
0340fcd
Compare
PastaPastaPasta
force-pushed
the
sec/v044
branch
from
August 23, 2026 15:20
afa7b4c to
33f8478
Compare
PastaPastaPasta
force-pushed
the
sec/v044
branch
from
September 23, 2026 21:00
9c6a0d1 to
b43e538
Compare
…'s window The quorum recency (bad-assetunlock-too-old-quorum) and height window (bad-assetunlock-too-late) checks depend on the validating node's tip: the same transaction is valid a block earlier or later, and version 2 unlocks are deliberately kept in the mempool past their window. Failing them as TX_CONSENSUS made MaybePunishNodeForTx score honest relayers Misbehaving(100). Fail them as TX_BAD_SPECIAL instead; blocks containing such a transaction are still rejected as BLOCK_CONSENSUS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…as valid in their window A version 2 asset unlock past its height window is kept in the mempool awaiting a re-signed instance, so its descendants survive. Nodes that did not hold it (after a restart via LoadMempool, or a new peer via BIP35 mempool sync or relay) validated it against their tip and rejected it, orphaning its descendants and splitting mempools. Validate such an instance against its own window instead: at its requestedHeight, or at the window's last height for a quorum mined after requestedHeight. Instances older than the mempool expiry are still validated against the tip. The instance stays unminable and is not InstantSend-locked until refreshed, since the miner and the lock signer validate against the tip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ValidateDemotionEntry never checks that the session denomination has a larger adjacent denomination, unlike its promotion counterpart. For a 10 DASH rebalance session GetLargerAdjacentDenom() returns 0, so IsValidInOuts() expects input denomination 0 and its input check then accepts exactly the non-denominated inputs it is meant to exclude. The entry balances (one 100.001 DASH input against ten 10.0001 DASH outputs), so nothing else rejects it and the session is only torn down once honest participants re-validate the final transaction and refuse to sign. Reject the entry with ERR_DENOM, consuming the collateral like every other malformed rebalance entry.
…0 feature v23.1.0 presented a new feature dust attack protection and shipped as GUI option. Release of v24 adds a new command-line option for dashd, wallet-side locking and a unified setting
…xtAddr Using a BasicBLS EvoNode to ExtAddr built IP:port Platform entries from its scalar ports and inserted them into the unique-property map. Nothing checked them beforehand, so an entry another masternode already held made UpdateMN() throw out of block assembly, and any peer could relay the update that triggered it. ExtAddr is unreleased, so nothing depends on that conversion. Drop the current version of ExtAddr as it is unreleased and nothign depends on that conversion: an EvoNode is raised to ExtAddr only by a ProUpServTx that lists its Platform entries, which the existing payload checks already validate against the map. A registrar update that would raise a BasicBLS EvoNode with addresses is rejected, and after v24 a pre-ExtAddr service update for an EvoNode is rejected regardless of the node's state, so the rule does not depend on transaction order within a block and needs no mempool bookkeeping. The reverse extraction of scalar ports from ExtNetInfo was unreachable and goes too.
The startup diff repair runs on a background thread after the node is live. Its final step, CompleteRepair(), opened an EvoDB transaction without cs_main, racing ConnectTip's transaction on the validation thread. Since EvoDB transactions became per-chainstate, BeginTransaction asserts that no transaction is open, so a node syncing while the repair finished aborted with.
Verification failed between snapshots at heights 2256192 and 2256768: Applied diffs do not match target snapshot
2026-09-24T14:19:38Z [ProcessBlock] h[2343646] numCommitmentsRequired[0] numCommitmentsInNewBlock[0]
2026-09-24T14:19:38Z Assertion failure:
assertion: !active_transaction.has_value()
file: evo/evodb.cpp, line: 72
function: std::unique_ptr<CEvoDBScopedCommitter> CEvoDB::BeginTransaction(EvoDbIdentity)
0#: (0x558043BED6AB) stacktraces.cpp:662 - __wrap___assert_fail
1#: (0x558042F07B43) evodb.cpp:77 - CEvoDB::BeginTransaction(EvoDbIdentity)
2#: (0x5580436672B2) deterministicmns.cpp:1312 - CDeterministicMNManager::CompleteRepair()
Before that, the marker write silently merged into the block's open transaction. EvoDB transactions are serialized by cs_main everywhere else, so require it for CompleteRepair() and take it at both call sites.
The check that refuses createwallet with descriptors given and load_on_startup omitted reads as an arbitrary coupling of two unrelated options, and was nearly relaxed to descriptors=true only on that reading. It guards against the v21 argument reordering: up to v20 the sixth positional argument was load_on_startup, v21 inserted descriptors in front of it, so a caller written for v20 now sets descriptors and leaves load_on_startup unset. Switching to descriptor wallets by default since dash#7204 makes v20-style false in that position silently creates a legacy wallet, which is why both values have to be refused, not only true. Name the two conditions and record the reason next to the check. No behaviour change.
…ver migrated Up to v23, ChainstateManager::InitAdditionalIndexes() rewrote the "addressindex", "spentindex" and "timestampindex" flags in the block index database from the current options on every load (v23.1.8: node/chainstate.cpp:134), right after the checks that refused to start with a re-enabled index unless the node reindexed. PR dashpay#7101 moved the indexes to the async framework: 18276a6 deleted InitAdditionalIndexes() together with those checks, and cc1dd3c in the same PR made MigrateOldIndexData() read the three flags to decide whether the legacy rows still cover the chain tip, on the assumption that they describe the last run. Nothing has written them since, so they are frozen at whatever the last v23 run stored. The result is silent index corruption, not a cosmetic one: a node that ran v23 with -addressindex, then v24 without it (migration skipped, chain advances, legacy rows fall behind), then v24 with it again still reads the flag as true, migrates the stale rows and stamps the current tip as the index locator in FinalizeMigration(). The blocks connected in between are missing from the index for good while it reports itself synced. That disable-then-enable sequence is exactly what became possible once dashpay#7101 dropped the reindex requirement, so v24 cannot ship with the inference as it stands. Write the flag of every disabled index as false at each start, before the migration decides anything. The rows of an enabled index are consumed by the migration on the same start, and a shutdown in the middle of it leaves that flag untouched so the remainder is still picked up.
Since the bitcoin#23508 backport (dashpay#6888, 0fa73c6) elapsed and count describe the period that contains pindex, while the Dash-only anchor for the dynamic threshold kept the earlier formula, which on the last block of a period resolves to pindex itself and therefore reports the threshold of the period the next block starts. Read side by side the anchor looks like an off-by-one left behind by the backport, and it was "fixed" to blocks_in_period once already, which fails dynamic_activation_thresholds_tests: that suite pins the threshold on the last block of attempt i to Threshold(i + 1), in line with status_next in the RPC output. Say so at the anchor, so the next reader knows the difference is intended and which test guards it.
…s, cj balance, versionbits, legacy indexes bd32275 doc(versionbits): explain the threshold anchor in GetStateStatisticsFor (Konstantin Akimov) 20f4af8 fix(index): record disabled legacy indexes so their stale rows are never migrated (Konstantin Akimov) 4a7881b refactor(rpc): explain the createwallet load_on_startup requirement (Konstantin Akimov) f3b5eba fix(coinjoin): reject demotion entries for the largest denomination (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented 1. legacy indexes. Up to v23, ChainstateManager::InitAdditionalIndexes() rewrote the "addressindex", "spentindex" and "timestampindex" flags in the block index database from the current options on every load (v23.1.8: node/chainstate.cpp:134), right after the checks that refused to start with a re-enabled index unless the node reindexed. PR dashpay#7101 moved the indexes to the async framework: 18276a6 deleted InitAdditionalIndexes() together with those checks, and cc1dd3c in the same PR made MigrateOldIndexData() read the three flags to decide whether the legacy rows still cover the chain tip, on the assumption that they describe the last run. Nothing has written them since, so they are frozen at whatever the last v23 run stored. The result is silent index corruption, not a cosmetic one: a node that ran v23 with -addressindex, then v24 without it (migration skipped, chain advances, legacy rows fall behind), then v24 with it again still reads the flag as true, migrates the stale rows and stamps the current tip as the index locator in FinalizeMigration(). The blocks connected in between are missing from the index for good while it reports itself synced. That disable-then-enable sequence is exactly what became possible once dashpay#7101 dropped the reindex requirement, so v24 cannot ship with the inference as it stands. 2. False-alarm for version bits by agents Since the bitcoin#23508 backport (dashpay#6888, 0fa73c6) elapsed and count describe the period that contains pindex, while the Dash-only anchor for the dynamic threshold kept the earlier formula, which on the last block of a period resolves to pindex itself and therefore reports the threshold of the period the next block starts. Read side by side the anchor looks like an off-by-one left behind by the backport, and it was "fixed" to blocks_in_period once already, which fails dynamic_activation_thresholds_tests: that suite pins the threshold on the last block of attempt i to Threshold(i + 1), in line with status_next in the RPC output. 3. False-alarm for createwallet by agents The check that refuses createwallet with descriptors given and load_on_startup omitted reads as an arbitrary coupling of two unrelated options, and was nearly relaxed to descriptors=true only on that reading. It guards against the v21 argument reordering: up to v20 the sixth positional argument was load_on_startup, v21 inserted descriptors in front of it, so a caller written for v20 now sets descriptors and leaves load_on_startup unset. But agents wrongly assumes that behavior should be fixed. This PR adds extra clarification to prevent false alarms in the future. 4. cj balance WalletModel::getAvailableBalance() answers a CoinJoin send from the cached anonymized balance when no coin is selected. That balance is accumulated by CachedTxGetAvailableCoinJoinCredits() without regard to LockCoin(), unlike the ordinary path right below, which subtracts the locked share. The send dialog therefore accepts an amount that coin selection cannot cover and the user only learns about it from the createTransaction() failure. 5. cj demolotions over max denomination ValidateDemotionEntry never checks that the session denomination has a larger adjacent denomination, unlike its promotion counterpart. For a 10 DASH rebalance session GetLargerAdjacentDenom() returns 0, so IsValidInOuts() expects input denomination 0 and its input check then accepts exactly the non-denominated inputs it is meant to exclude. The entry balances (one 100.001 DASH input against ten 10.0001 DASH outputs), so nothing else rejects it and the session is only torn down once honest participants re-validate the final transaction and refuse to sign. ## What was done? - fix(index): record disabled legacy indexes so their stale rows are never migrated - doc(versionbits): explain the threshold anchor in GetStateStatisticsFor - refactor(rpc): explain the createwallet load_on_startup requirement - fix(coinjoin): reject demotion entries for the largest denomination Fix "don't report locked mixed coins as spendable for CoinJoin sends" is excluded from PR as not working fix. Could be fixed later by other PR. ## How Has This Been Tested? Run unit & functional tests. CJ demotions had one extra regression test. ## Breaking Changes N/A ## 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: 35a75f4d18616f91ae5ebc97066ddec5a349b0672c81dacd6fa03e9a918fa15a758f2efcb4fe005aff2aace4df94bb1fea1c5bf4417168415c7a9b8f671cbe93
…ssing release notes for various features d1957f8 docs: add release notes for use_cj on the remaining spending RPCs (Konstantin Akimov) db036aa docs: add release notes for quorum dkgstatus upcoming_dkgs (Konstantin Akimov) ea2be11 docs: add release notes for the stricter bind failure handling (Konstantin Akimov) 6c500b5 docs: add release notes for the removed gettxoutsetinfo bip30 field (Konstantin Akimov) 4fa7d8f docs: list every RPC affected by the deprecated service field (Konstantin Akimov) 6ca3c76 docs: describe -dustprotectionthreshold as an extension of the v23.1.0 feature (Konstantin Akimov) 90d98b9 docs: cover numeric RPC arguments in the 7099 release notes (Konstantin Akimov) 69d330b docs: add release notes for the removed -deprecatedrpc=fees option (Konstantin Akimov) 6339f9c docs: add release notes for multi-party masternode payouts (Konstantin Akimov) 970fdf5 docs: add release notes for Asset Lock v2 and DIP-18 Platform addresses (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented V24 release is coming and some features and breaking changes doesn't have release notes. ## What was done? - add release notes for use_cj on the remaining spending RPCs - add release notes for quorum dkgstatus upcoming_dkgs - add release notes for the stricter bind failure handling - add release notes for the removed gettxoutsetinfo bip30 field - list every RPC affected by the deprecated service field - describe -dustprotectionthreshold as an extension of the v23.1.0 feature - cover numeric RPC arguments in the 7099 release notes - add release notes for the removed -deprecatedrpc=fees option - add release notes for multi-party masternode payouts - add release notes for Asset Lock v2 and DIP-18 Platform addresses ## How Has This Been Tested? N/A ## Breaking Changes N/A ## 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: 0946c2f717fd34af29cc4b468d4e817d39af7672a9b4d62f1fd3e57b20c3722887f3eef67d61312601e4f5cb50f3457225816cc61a34a34b203758e9ae946db6
…alizing their relayers 68b7d95 doc: release notes for admitting expired version 2 asset unlocks (pasta) be272c2 fix(mempool): admit expired version 2 asset unlocks whose signature was valid in their window (pasta) 6655c50 fix(assetlock): do not punish relays of asset unlocks outside the tip's window (pasta) Pull request description: ## Issue being fixed or feature implemented Since dashpay#7639, the mempool keeps a version 2 asset unlock after its 48-block height window closes, waiting for Platform to re-sign it. That way the withdrawal's spends survive a re-sign. However, only the nodes that accepted the unlock while it was still valid hold it. Every other node validates it against its own tip, and that caused three problems once v24 is active: - **Honest peers get discouraged.** A node without the instance, for example a restarted node doing its default `-syncmempool` BIP35 request to outbound peers, rejects it with `bad-assetunlock-too-late` or `bad-assetunlock-too-old-quorum`. Both were `TX_CONSENSUS`, so `MaybePunishNodeForTx` scored the honest relayer `Misbehaving(100)` and it was discouraged and disconnected. I reproduced this on develop 06582cd. `feature_asset_locks.py` never caught it because it runs with `-whitelist=127.0.0.1`, which gives every test peer `noban`. - **Descendants are orphaned.** Spends of the withdrawal were stuck as orphans on those nodes and dropped after 20 minutes. As a result, mempools diverged across the network. - **Restarts lose the unlock.** `LoadMempool` re-validates each saved transaction, so a node dropped its own expired instance at every restart and orphaned the children. The same tip dependence already affected v1 unlocks: a peer one block ahead of the relayer (or a node receiving a fresh unlock before it has caught up to `requestedHeight`) punished honest relayers. ## What was done? 1. `fix(assetlock)`: `bad-assetunlock-too-late` and `bad-assetunlock-too-old-quorum` now fail as `TX_BAD_SPECIAL` instead of `TX_CONSENSUS`, so a relayer isn't punished. Both depend on the validating node's tip, so an honest peer at another height relays such a transaction. - Blocks are unaffected: `ProcessSpecialTxsInBlock` already accepts either result and rejects the block as `BLOCK_CONSENSUS`. - A bad signature, unknown quorum hash or duplicated index is still `TX_CONSENSUS` and scores 100. 2. `fix(mempool)`: `PreChecks` now validates an expired version 2 unlock against its own height window instead of the tip. It is admitted if it was valid at `requestedHeight`, or at the window's last height when the signing quorum was mined after `requestedHeight`. - Instances whose `requestedHeight` is older than the mempool expiry, converted to blocks, are still checked against the tip and rejected. Holders time-evict unlocked entries on that schedule, and the limit caps how far back a peer can make us scan quorums. - Such an instance still isn't mined or InstantSend-locked until it is refreshed. The miner (`BlockAssembler`) and `CheckCanLockAssetUnlock` both validate against the tip, and its children can't be locked while their parent is unlocked and unmined. - `TryAssetUnlockRefresh` still requires a fresher, minable instance, so an expired instance can't replace a held one. The one-instance-per-index rule and the mined-index check run before the signature check, as before. - `LoadMempool` and reorg re-admission go through the same path, so the restart gap is closed too. 3. `doc`: the release note for dashpay#7639 now describes both behaviours. Notes for review: - A peer can now make a node scan quorums at old heights within the mempool-expiry window (about 8064 blocks on mainnet) at no cost when the result is `too-old-quorum`. Each scan is bounded and uses the existing `ScanQuorums`/`GetQuorum` caches. A bad signature is still punished, and each rejected instance hash lands in `m_recent_rejects`. - For nodes that don't hold an expired instance, `sendrawtransaction`/`testmempoolaccept` now succeed instead of returning `bad-assetunlock-too-late`. Nodes that hold it already returned success. ## How Has This Been Tested? New steps in `feature_asset_locks.py`, each observing the P2P or mempool outcome: - An unlock outside the tip's window relayed over P2P is rejected with `bad-assetunlock-too-late`, and no `Misbehaving` line is logged. That line is logged even for `noban` peers. - A node restarted with an empty mempool (`-persistmempool=0`) accepts two expired v2 instances and a child over P2P, without `Misbehaving`. - A forged expired instance (tampered index, so the signature no longer verifies) is rejected with `bad-assetunlock-not-verified` and still scores `(0 -> 100)`. - A `requestedHeight` that overflows the height window is rejected with `bad-assetunlock-too-late`. - An expired instance signed by a quorum mined after its `requestedHeight` is accepted (`testmempoolaccept`). - The expired instances and the child survive a normal restart (`mempool.dat` reload). - The existing later steps still pass: they check that the expired instances aren't locked, and that a refresh gets locked and mined. Mutation checks, run locally: - Reverting commit 1's source hunk makes the new commit 1 step fail on `Misbehaving`. - Reverting commit 2's source hunk makes both the P2P admission step and, separately, the restart step fail. - Dropping the retry at the window's last height makes the late-quorum step fail. Also ran `feature_asset_locks.py`, `feature_llmq_signing.py`, `feature_llmq_is_retroactive.py`, `mempool_persist.py`, `test_dash --run_test=evo_assetlocks_tests,evo_islock_tests`, and `lint-python.py`. All were run on macOS arm64 with a debug build. ## Breaking Changes None in consensus: blocks are validated exactly as before. The changes are to mempool policy and P2P scoring, and they only affect asset unlock transactions. ## Checklist: - [x] I have performed a self-review of my own code - [x] 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: 476ffd86f07b0cd101fe55cbe5730d4e6d95592eb4d757934421b84e4ced6fb85bce98cee96b437ef95833c28698730968e5fa235c117163a776780230c7feca
a2de4c5 fix(evo): take cs_main for CompleteRepair (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented The startup diff repair runs on a background thread after the node is live. Its final step, CompleteRepair(), opened an EvoDB transaction without cs_main, racing ConnectTip's transaction on the validation thread. Since EvoDB transactions became per-chainstate, BeginTransaction asserts that no transaction is open, so a node syncing while the repair finished aborted with. Verification failed between snapshots at heights 2256192 and 2256768: Applied diffs do not match target snapshot 2026-09-24T14:19:38Z [ProcessBlock] h[2343646] numCommitmentsRequired[0] numCommitmentsInNewBlock[0] 2026-09-24T14:19:38Z Assertion failure: assertion: !active_transaction.has_value() file: evo/evodb.cpp, line: 72 function: std::unique_ptr<CEvoDBScopedCommitter> CEvoDB::BeginTransaction(EvoDbIdentity) 0#: (0x558043BED6AB) stacktraces.cpp:662 - __wrap___assert_fail 1#: (0x558042F07B43) evodb.cpp:77 - CEvoDB::BeginTransaction(EvoDbIdentity) 2#: (0x5580436672B2) deterministicmns.cpp:1312 - CDeterministicMNManager::CompleteRepair() ## What was done? Before that, the marker write silently merged into the block's open transaction. EvoDB transactions are serialized by cs_main everywhere else, so require it for CompleteRepair() and take it at both call sites. ## How Has This Been Tested? Intermittent failure, happened only once. ## Breaking Changes N/A ## 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: e29b944b0473cba598d89cba5658bff6b7eabcb70bdc562932720b656052b7b43c7483994450cd8610c622e1ae5acc97afdc23113f79b0d0e5a78e91cac47ec3
…sing an EvoNode to ExtAddr 22c41b6 fix(evo): stop deriving Platform entries when raising an EvoNode to ExtAddr (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented Using a BasicBLS EvoNode to ExtAddr built IP:port Platform entries from its scalar ports and inserted them into the unique-property map. Nothing checked them beforehand, so an entry another masternode already held made UpdateMN() throw out of block assembly, and any peer could relay the update that triggered it. It's alternate simpler solution to dashpay#7743 - instead resolving conflicts it maybe better to prevent them. ## What was done? ExtAddr is unreleased, so nothing depends on that conversion. Drop the current version of ExtAddr as it is unreleased and nothign depends on that conversion: an EvoNode is raised to ExtAddr only by a ProUpServTx that lists its Platform entries, which the existing payload checks already validate against the map. A registrar update that would raise a BasicBLS EvoNode with addresses is rejected, and after v24 a pre-ExtAddr service update for an EvoNode is rejected regardless of the node's state, so the rule does not depend on transaction order within a block and needs no mempool bookkeeping. The reverse extraction of scalar ports from ExtNetInfo was unreachable and goes too. ## How Has This Been Tested? Regressions tests are updated. ## Breaking Changes No breaking changes so far as v24 had not been released yet. ## 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)_ ACKs for top commit: kwvg: utACK 22c41b6 Tree-SHA512: aa009dbf69faf18e02a2c03758f0f8cfffb2e7bcd39fc0af4a58a8f7f27f18cc6e87879afd865f79295a9600a7d54ae1562a2953f738ba33b91689634965912c
…etqrinfo CleanupCache() only runs from DoMaintenance() after a new block has been processed. Between blocks nothing bounds mnListsCache and mnListDiffsCache, so a peer sending getmnlistd/getqrinfo for many historical blocks adds a disk snapshot, up to 575 diffs and the mini-snapshots of every requested range, and the node keeps all of it until the next block arrives. After building either response, drop cached lists and diffs older than LIST_DIFFS_CACHE_SIZE below the tip, the entries CleanupCache() would drop anyway. The trim runs in the P2P handlers rather than in GetListForBlock(), which validation and RPC also call: an in-window request, block connection and historical RPC walks keep their cache entries exactly as before. Based on the approach of dashpay#7746 by Konstantin Akimov. Co-Authored-By: Konstantin Akimov <github@knstqq.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PastaPastaPasta
force-pushed
the
sec/v044
branch
from
September 24, 2026 19:34
b43e538 to
29dad5d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit finding V044. An unauthenticated peer can grow
CDeterministicMNManager's in-memory caches without bound viagetmnlistd.Issue
mnListsCacheandmnListDiffsCachewere only trimmed byCleanupCache(), which runs when a new block arrives. Between blocks there is no bound at all: a peer spamminggetmnlistdfor historical blocks drivesGetListForBlockto append an entry per requested block, and a full mainnet MN list is several MB. No proof of work, no authentication, no rate limit on the request side.Fix
Bound admission two ways, in
src/evo/deterministicmns.{cpp,h}:ShouldRetainCacheHeight()— do not retain at all any height older than the recency windowCleanupCachewould have dropped anyway (height + LIST_DIFFS_CACHE_SIZE >= tipIndex->nHeight). Retains freely before the tip is known, for early startup.EnforceListsCacheLimit()/EnforceDiffsCacheLimit()— hard caps (MAX_CACHE_LISTS = 256,MAX_CACHE_DIFFS = LIST_DIFFS_CACHE_SIZE + 64) that evict the oldest-height entry, never the tip snapshot.Admission is funneled through new
CacheMNList()/CacheMNListDiff()helpers so no call site can bypass the bound.MAX_CACHE_LISTSis sized well above honest steady-state usage (tip + live quorum bases + mini-snapshots within the recency window).Tests
test: prove mnListsCache grows unboundedly via historical GetListForBlockprecedes the fix and fails without it.GetListCacheSize()/GetListDiffsCacheSize()accessors expose real production state rather than adding test-only mutation hooks.Review notes
The third commit is a review follow-up worth reading on its own: the initial fix evicted diffs during the rebuild walk, so a diff the apply loop still needed could be evicted mid-walk, landing on a bare
assert(false)— a remotely-triggerable crash, i.e. worse than the DoS being fixed. The follow-up removes the mid-walk eviction, admits every diff the walk reads unconditionally, and enforces the cap once the walk is done. It also deletes theassert(false)path.Two locking details, both deliberate:
CacheMNListprefersemplaceover assignment becauseCDeterministicMNList::operator=locksm_cached_sml_mutex, which must not run whilecsis held (lock-order checker).EXCLUSIVE_LOCKS_REQUIRED(cs)withAssertLockHeld.Based on
dashpay/dashdevelop @6d04c60ef36. Not rebase-tested against any newer tip; full functional suite not run.🤖 Generated with Claude Code