Skip to content

fix: bound mnListsCache admission to stop getmnlistd memory DoS - #62

Draft
PastaPastaPasta wants to merge 25 commits into
developfrom
sec/v044
Draft

PastaPastaPasta wants to merge 25 commits into
developfrom
sec/v044

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

Audit finding V044. An unauthenticated peer can grow CDeterministicMNManager's in-memory caches without bound via getmnlistd.

Issue

mnListsCache and mnListDiffsCache were only trimmed by CleanupCache(), which runs when a new block arrives. Between blocks there is no bound at all: a peer spamming getmnlistd for historical blocks drives GetListForBlock to 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 window CleanupCache would 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_LISTS is 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 GetListForBlock precedes 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 the assert(false) path.

Two locking details, both deliberate:

  • CacheMNList prefers emplace over assignment because CDeterministicMNList::operator= locks m_cached_sml_mutex, which must not run while cs is held (lock-order checker).
  • All new helpers are EXCLUSIVE_LOCKS_REQUIRED(cs) with AssertLockHeld.

Based on dashpay/dash develop @ 6d04c60ef36. Not rebase-tested against any newer tip; full functional suite not run.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sec/v044

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PastaPastaPasta and others added 23 commits September 23, 2026 23:24
…'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
PastaPastaPasta and others added 2 commits September 24, 2026 12:46
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants