Skip to content

backport: assumeutxo M4 — evo snapshot format v3 and LLMQ reconstruction - #53

Open
PastaPastaPasta wants to merge 122 commits into
assumeutxo/m3-background-completionfrom
assumeutxo/m4-evo-snapshot
Open

backport: assumeutxo M4 — evo snapshot format v3 and LLMQ reconstruction#53
PastaPastaPasta wants to merge 122 commits into
assumeutxo/m3-background-completionfrom
assumeutxo/m4-evo-snapshot

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Issue being fixed or feature implemented

Upstream UTXO-only snapshots can't validate a DIP3-active chain like Dash's:
masternode lists, LLMQ quorums/rotation, the credit pool, and EHF state all
need to be captured and reconstructible from a snapshot, independently
verifiable against the chain (not merely trusted). This is milestone 4/7, the
largest design unit of the series (10 review rounds; design docs D1/D2). The
execution plan and per-unit ledger (M4 section) are tracked outside the repo
in a private gist, not checked in: https://gist.github.com/PastaPastaPasta/aa52b1f89fb74a0566ba3b5e15afab6a
loadtxoutset is deliberately still unexposed after this PR — it needs the
format landed here, but the RPC itself arrives in M5.

PR 4/7 in the stacked series — base is M3 (assumeutxo/m3-background-completion).

This PR implements the user-locked design decision (2026-07-11): the evo
snapshot carries the FULL quorum member-reconstruction history, diff-encoded
from the base MN list, plus per-work-block score modifiers/CbTx signatures —
chosen over a smaller verification-only format because both rotated and
non-rotated GetHashModifier need CbTx data unavailable pre-completion, and
reconstructing the active non-rotated set needs MN lists ~24 cycles (~576
blocks) to ~2,304 blocks back, depending on quorum type.

What was done?

  • B4.1 — evo snapshot format v3 + evo_hash: CEvoSnapshot v3 replaces
    full historical lists with one canonical reverse diff chain from the base
    list through every required LLMQ work block (predecessor/target hashes,
    height, registration counter, canonical target hash, bounded canonical
    CDeterministicMNListDiff per link; exact (llmq_type, work_block_hash, modifier) tuples for every commitment/rotation reference). A pure horizon
    enumerator covers non-rotated active+safety cycles and rotated H-C..H-4C
    (worst default span 2,312 blocks; 104 on regtest), with a params-derived
    link count cap (192 max with currently enabled LLMQs) and a shared
    786,432-operation decode budget. Five review rounds.
  • B4.2 — dump emission + populate/seed v3: dumptxoutset builds the v3
    section under the coins-cursor cs_main pin; populate validates ancestry,
    exact history/modifier coverage, work-block relationships, evo_hash, and
    CbTx roots, then atomically seeds reconstructed lists, modifiers,
    commitments, rotation snapshots, credit pool, and MNHF state under the
    snapshot EvoDB identity. A modifier-mismatch signal is contained at quorum,
    connect/disconnect, direct-validity, VerifyDB, and replay boundaries and
    routes to the controlled invalid-snapshot/shutdown path. 3 review rounds.
  • B6 — init-order integration: reindex modes remove all snapshot
    lifecycle dirs before the EvoDB wipe/detection; an assumeutxo prune lock
    protects the base block until deferred CbTx/evo checks complete. Round 2
    review found a released prune lock resurrected by DisconnectTip rewind —
    fixed via BlockManager::DeletePruneLock.
  • Circular-deps refactor: 6 new cycles introduced by the M4 work were
    broken by extracting evo/snapshot_types.h and moving snapshot
    load/seed/completion orchestration into evo/snapshot_load.cpp (note for
    future backports: upstream hunks touching PopulateAndValidateSnapshot /
    MaybeCompleteSnapshotValidation in validation.cpp must be retargeted
    there).
  • Tests prove diff determinism, cumulative bounded decode, exact modifier
    cross-checking, and rotated/non-rotated GetAllQuorumMembers
    reconstruction with block data unavailable, using an independent manager
    oracle; removing a seed trips the fail-closed NORMAL-state barrier
    (also covers ScanQuorums and recovered-signature verification).

How Has This Been Tested?

  • make check: exit 0, 0 failures (rerun on the final post-refactor tree).
  • Functional battery (12): feature_assumeutxo_dash, feature_dip3 ×2,
    feature_llmq_signing ×2, feature_llmq_rotation,
    feature_llmq_chainlocks, feature_mnehf, feature_asset_locks,
    feature_init, feature_reindex, feature_pruning — all passed.
  • Lint: circular-dependencies PASS, whitespace PASS; python lint deferred to CI.
  • Milestone gate PASS on 2026-07-11.

Breaking Changes

Snapshot format is new/versioned (v3) and unreleased; no compatibility
surface exists yet. loadtxoutset remains unexposed to end users until M5.

Checklist:

  • I have performed a self-review of my own code
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation (docs rewrite lands in M7/B8)
  • I have assigned this pull request to a milestone

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a0c57171-548c-46e8-a625-d0eb1924febf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch assumeutxo/m4-evo-snapshot

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
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 32d5966 to 4f9d716 Compare July 12, 2026 14:21
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from 52a0371 to ed610de Compare July 12, 2026 14:29
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 4f9d716 to f08bce5 Compare July 12, 2026 16:00
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from ed610de to 609b604 Compare July 12, 2026 16:01
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from f08bce5 to a6e49d6 Compare July 13, 2026 22:45
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from 609b604 to 11d1fb7 Compare July 13, 2026 22:45
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from a6e49d6 to eb757a0 Compare July 14, 2026 05:10
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch 2 times, most recently from 022d3e6 to 590e34b Compare July 14, 2026 17:01
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from eb757a0 to 2bc543c Compare July 14, 2026 17:01
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from 590e34b to 9e0f8d7 Compare July 20, 2026 16:59
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 2bc543c to 5b3d0d7 Compare July 20, 2026 16:59
UdjinM6 and others added 6 commits July 28, 2026 21:00
Operator key uniqueness was enforced per BLS encoding rather than per key.
mnUniquePropertyMap is keyed by GetUniquePropertyHash(), which serializes its
argument, and a BLS key serializes differently under the legacy and basic schemes.
So one public key sits in one of two possible slots, and CheckProRegTx's duplicate
check -- which consults that map -- could not see the same key presented under the
other encoding. A ProRegTx never proves ownership of the operator key, so anyone
could re-register an existing masternode's operator public key for the price of a
collateral.

Rather than re-key the map canonically, which would apply retroactively -- the map is
derived, not stored: Unserialize() clears it and rebuilds via AddMN, so a historical
cross-scheme pair would make AddMN throw and nodes fail to sync -- probe both
encodings at each point where a key can be claimed. There are exactly two schemes, so
this is two O(1) lookups rather than a scan, and the consensus rules are gated on v24,
leaving historical reconstruction untouched.

Checking only the confirmed list is not enough, so probes are placed at every point a
key is claimed:

 - CheckProRegTx and CheckProUpRegTx, against the previous block's list.
 - RebuildListFromBlock, against the list as rebuilt so far, since per-transaction
   checks run against pindexPrev and are blind to each other within a block. This
   rejects cleanly: AddMN()/UpdateMN() report duplicates by throwing, and that throw
   would escape block-template assembly.
 - AcceptToMemoryPool, so two in-flight transactions cannot claim one key under
   different schemes. This one is deliberately NOT gated on v24. Block assembly does
   not revalidate special transactions cumulatively -- it checks each candidate
   against the tip, where neither key is yet present -- so a pair admitted before
   activation is never evicted and would still be selected together afterwards,
   leaving an honest miner unable to build any template at all. Keeping the pair out
   of the mempool is what actually closes that, and mempool policy is allowed to be
   stricter than consensus: a node rejecting the second transaction still accepts a
   block containing it, so no chain can split over it.

The registrar probes run only when the operator key is actually changing, at every
layer. An update that keeps its own key cannot create a duplicate, and probing it
anyway would let a cross-scheme pair formed before activation permanently block the
affected masternode's registrar updates unless it rotated its key -- making an old
squat more harmful rather than less.

Pairs that already exist before activation are tolerated: nothing rehashes the map,
and they remain upgradable because leaving LegacyBLS rotates the key anyway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopts the maintainer's preferred approach from dashpay#7472 (re-encode the operator key
on a version change) instead of forcing a key rotation to leave LegacyBLS, while
keeping the cross-scheme uniqueness guards this branch added so the re-key cannot
collide and stall block production.

A masternode operator can keep the same BLS private key across the legacy->basic
migration; only the serialized encoding of the public key changes. Rather than
rejecting a same-key ProUpServTx/ProUpRegTx and forcing a rotation (which also
PoSe-bans the masternode), SetStateVersion() now re-encodes the stored key to the
scheme its version implies, and UpdateUniqueProperty() re-keys the scheme-dependent
unique-property map when the encoding changes. The RPCs build a BasicBLS migration
payload for a legacy masternode instead of erroring.

Because dashpay#7472's re-encode collides -- and UpdateMN() throws out of block assembly --
when a squatter already holds the same key under the other encoding (the live
per-encoding registration hole), migration is guarded: CheckProUpServTx,
CheckProUpRegTx and RebuildListFromBlock reject a migration that would collide with
another masternode's key under either scheme (bad-protx-dup-key), and only when the
key actually changes or the version crosses the scheme boundary, so a grandfathered
cross-scheme pair's non-migrating routine update is not blocked.

CDeterministicMNStateDiff also has to capture the re-encoding: its field comparison
used CBLSLazyPublicKey::operator==, which ignores the scheme, so a same-key
migration produced a diff that omitted the key. A node reconstructing the list from
evoDB diffs then kept the old encoding while an online-built list had the new one --
a reconstruction split that full-snapshot serialization does not reveal. The diff
now compares the scheme-dependent hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ness

Unit tests in evo_deterministicmns_tests.cpp and a functional test extension in
feature_dip3_v19.py for the two fixes on this branch: cross-scheme operator-key
uniqueness and in-place legacy->basic migration that keeps the same key. Each
rejection test was watched fail first by flipping the v24 activation height.

Covers the desync and evoDB-diff reconstruction paths, the mempool and same-block
cross-scheme pairings, the migration collision guards, the block-template stale
special-tx recheck, the HasOperatorKeyUnderAnyScheme helper, and the pre-v24
non-retroactivity guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hoist the repeated v24-activation scaffolding (tip/sync-dmn-tip lambdas
and the activation-mining loops) shared across the BLS scheme-migration
regressions into a small TestMNChainSetup fixture exposing Tip(),
IsV19Active(), IsV24Active(), ProcessBlock(), MineToV19() and MineToV24().
Each test body drops the boilerplate in favor of these helpers.

Pure test refactor, no behavior change; all 32 evo_dip3_activation_tests
cases still pass. Addresses the review request to reduce the verbosity and
future maintenance burden of the regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the existing slow_mode parameter through to activate_by_name
and use batch-50 on both DIP0008 activation paths. activate_by_name
already syncs after every batch and disables SPORK_17, so batch-50
matches the former batch-10 behavior without the post-reorg catch-up
risk that once justified slow_mode=True.
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch 2 times, most recently from e0377b0 to 9baa44d Compare August 1, 2026 19:55
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 569913c to 05723b8 Compare August 1, 2026 22:04
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from 9baa44d to ab27f8b Compare August 1, 2026 22:04
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 05723b8 to 667611e Compare August 1, 2026 22:27
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from ab27f8b to 2a968e5 Compare August 1, 2026 22:27
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 667611e to 328d224 Compare August 1, 2026 22:44
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch 2 times, most recently from ea58a7f to ed41e4e Compare August 1, 2026 23:09
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch from 328d224 to f9f8fa6 Compare August 1, 2026 23:35
PastaPastaPasta and others added 4 commits August 11, 2026 10:21
…ests

7ec9439 fix: let wallet know that it created non-standard tx that can't be send (Konstantin Akimov)
e6c074f test: add test for non-standard asset-locks (Konstantin Akimov)
bcea9eb fix: subtractfeefromamount can't be applied for platform addresses (Konstantin Akimov)
0f10445 test: more functional tests for asset locks v2 and small simplification (Konstantin Akimov)
2f79912 test: functional tests for asset-locks v2 (Konstantin Akimov)
69326ab feat: add support of platform addresses in CreateTransaction with wallet (Konstantin Akimov)
3a0fe55 feat: add support of platform addresses to validateaddress (Konstantin Akimov)
1fde46a feat: print asset lock txes with dash1... address for rpc outputs (Konstantin Akimov)
ad088f1 test: regression tests for asset lock v2 (Konstantin Akimov)
291e6ee feat: add v2 asset lock tx (Konstantin Akimov)

Pull request description:

  ## Issue being fixed or feature implemented
  Implements asset lock txes v2, see dashpay/dips#182

  ## What was done?
   - Added consensus changes for v2, activated by fork v24
   - implemented asset lock v2
   - added `address` field for ToJson helper for asset locks
   - added support of dash1.... tdash1... addresses as a recipient for spending RPCs
   - regressions tests for assetlocks v2
   - functional tets for assetlocks v2

  Also prior works: dashpay#7285, dashpay#7284

  ## How Has This Been Tested?
  See updates for unit & functional tests

  ## Breaking Changes
  Changes consensus rules for v24 activation to enable assetlock txes v2.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] 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: 3955575e030b7d82b6ec472634e6f72aa62a2d98e19be553cdf51d4de037da42da636c85ea8cbdb7609f86168f832dc1e957e74da5745bcd5609c7681f33e4c6
51f4049 perf(index): reuse compact filter file handles (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  Serving a normal BIP157 `getcfilters` range caused `BlockFilterIndex::LookupFilterRange` to open and close the compact-filter flat file once per filter. A 1,000-filter request therefore performed up to 1,000 file-open operations even when the filters were contiguous in the same underlying file.

  This adds avoidable filesystem overhead to mobile/SPV sync, where peers commonly request long contiguous ranges.

  ## What was done?

  - Keep one `AutoFile` open while reading consecutive filters from the same flat file.
  - Seek the reused handle to each recorded filter position.
  - Open a new handle only when the range crosses into another flat file.
  - Preserve checksum verification and `BlockFilter` construction in a shared `ReadFilterFromFile` helper.
  - Add focused benchmarks for single-filter lookup, 100/1,000-filter ranges, encoded-byte throughput, serialization, and combined lookup plus serialization.

  The wire protocol and individual `cfilter` messages are unchanged.

  ## How Has This Been Tested?

  Tested on Apple Silicon using a depends-based release build:

  - Full `make` build
  - Full `make check`
  - `test/functional/test_runner.py p2p_blockfilters.py rpc_getblockfilter.py`
  - `test/lint/lint-whitespace.py`
  - `test/lint/lint-circular-dependencies.py`

  ### In-tree benchmark

  For a contiguous 1,000-filter range containing approximately 2.64 MB of encoded filters:

  | Benchmark | Before | After | Improvement |
  |---|---:|---:|---:|
  | Range lookup | 21.21 ms | 5.96 ms | 3.56x / 71.9% less time |
  | Lookup + serialization | 21.26 ms | 6.19 ms | 3.43x / 70.9% less time |
  | Single-filter lookup | 21.23 µs | 23.91 µs | Effectively unchanged |

  The current branch rerun measured approximately 180,000 filters/s for the 1,000-filter range.

  ### End-to-end sync measurement note

  Exploratory rust-dashcore testnet sync runs confirmed that this path is exercised during full SPV sync, but their absolute before/after timings are not presented as proof here. The runs were not sufficiently controlled for server warm-cache state and background compact-filter-index I/O, so cross-condition timing differences were confounded. The deterministic in-tree benchmark above isolates the changed lookup path.

  ## 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
  - [x] 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

  This pull request was created by Codex.

Top commit has no ACKs.

Tree-SHA512: e0d3f710b807aba596e10ffe8e36a1c095cdfbedc3860a2a4ca8fc54d11870de00d915c5b1c4a1ecd61ab3fa5b64e9f60182b6900f270cc030ce9ade5a44ec4b
0a8c4da fix: stop caching invalid governance votes (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  Governance cached permanently rejected votes by their semantic vote hash, which excludes the signature. A forged signature cached first could therefore suppress a later legitimate signature over the same vote fields.

  The cache did not provide meaningful protection against adaptive verification work: a peer can always produce a fresh cache miss by changing the signature, timestamp, or parent hash. Invalid votes already carry a misbehavior penalty of 20, discouraging a peer after five failures.

  ## What was done?

  - Removed invalid-vote cache lookup and insertion from vote processing.
  - Removed the in-memory invalid-vote cache and its key-rotation cleanup.
  - Preserved the existing `governance.dat` version and field layout by consuming legacy invalid-cache entries during deserialization and writing the historical field back empty.
  - Added ECDSA and BLS regressions proving a forged vote cannot suppress a legitimate vote with the same semantic hash.
  - Added persistence coverage proving legacy invalid-cache entries are discarded without losing following governance objects.

  Valid-vote inventory and deduplication remain keyed by the existing semantic vote hash.

  ## How Has This Been Tested?

  - `make -j4`
  - `./src/test/test_dash --run_test=governance_vote_processing_tests`
  - `./src/test/test_dash --run_test=cachemap_tests`
  - `make check -j4`
  - `test/lint/all-lint.py`

  Tested on macOS arm64 using the repository's depends toolchain.

  ## Breaking Changes

  None. This does not change vote serialization, inventory hashes, protocol versions, database versions, consensus rules, or valid-vote deduplication.

  ## Checklist:

  - [x] I have performed a self-review of my own code
  - [x] 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

  This pull request was created by Codex.

Top commit has no ACKs.

Tree-SHA512: ad4c8a1e8dc41c1a88ba3031fb8ae09a44a14325f7b4d635232295e3d4f65e4f24514704554f1715cedf4ac429977e54f47afc47ed287d7d64c3a5e4d356e34d
The asan job reports leaks owned by two dependencies: Berkeley DB's lock
table and memory-pool file metadata, and Qt's process-global DBus
connection manager.

For Berkeley DB, do what upstream did in 04a7a7a and stop building it in
this job. Upstream's asan job carried -DWITH_BDB=ON and libdb5.3++-dev until
that commit, ran the same tool_wallet.py and wallet_hd.py, and never needed
a Berkeley DB entry in test/sanitizer_suppressions/lsan; the file has never
contained one. BerkeleyEnvironment::Close() here is identical to the last
upstream version, so the difference is not wallet code but the dependency:
upstream linked the system libdb 5.3 shared library, while this job takes
DEP_OPTS="" and so links 4.8.30 statically out of depends. The msan job is
already configured the same way.

Qt still needs a suppression, and by symbol rather than by module. Upstream
suppresses Qt with leak:libQt6Widgets, which only works for a shared
library; with Qt linked statically from depends the allocation belongs to
the executable and that form cannot match. Upstream hit this in 5be31b2
and answered it with per-symbol rules, so add QDBusConnectionManager next to
the two symbols already taken from that commit.
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from abb7a0c to 08ac564 Compare August 11, 2026 16:01
… and getaddressinfo

validateaddress deliberately omitted scriptPubKey for DIP-18 Platform addresses while getaddressinfo reported the derived credit output script along with ismine and isscript, so the two RPCs told inconsistent stories about the same address. Align them: validateaddress now also reports the credit output script an asset lock would carry for the address (plus isscript), with help text spelling out the semantics, and getaddressinfo's help clarifies that ismine/solvable refer to that script rather than to Platform identity ownership.
…y in validateaddress and getaddressinfo

e8d0420 fix(rpc): describe Platform addresses consistently in validateaddress and getaddressinfo (pasta)

Pull request description:

  ## Issue being fixed or feature implemented
  Follow-up to dashpay#7294. Built on top of that PR's branch; **only the last commit is new here** — please review just `fix(rpc): describe Platform addresses consistently in validateaddress and getaddressinfo`.

  With dashpay#7294, the two address-introspection RPCs tell inconsistent stories about the same DIP-18 Platform address: `validateaddress` deliberately omits `scriptPubKey` ("a Platform address has no layer-1 output script"), while `getaddressinfo` describes the address against the credit output script an asset lock would carry for it, including `scriptPubKey`, `isscript` and `ismine` — and `ismine: true` there is easy to misread as Platform identity ownership.

  ## What was done?
  - `validateaddress` now also reports the credit output script (`scriptPubKey`) and `isscript` for a valid DIP-18 Platform address, matching `getaddressinfo`, with help text spelling out the semantics.
  - `getaddressinfo` help now clarifies that `ismine`/`solvable` refer to the derived credit output script, not to ownership of a Platform identity.

  ## How Has This Been Tested?
  Updated `rpc_invalid_address_message.py`: `check_platform` now asserts the exact `scriptPubKey` and `isscript` for the DIP-18 P2PKH and P2SH test vectors (the P2SH script hash constant is decoded from the DIP-0018 test vector), alongside the existing normalization and error-field checks.

  ## Breaking Changes
  `validateaddress` output for DIP-18 Platform addresses gains `scriptPubKey` and `isscript` fields (previously absent). No behavior change for regular addresses.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] 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)_

ACKs for top commit:
  knst:
    utACK e8d0420

Tree-SHA512: 7591b06ec68dc01b302066ff81e040419d24d3d1302d0f98b8e970849d34c1c175ebc9ab68cd24b2599c837bc5e8d8d48e44d484a43ec49de360cd2581b49b15
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from 08ac564 to aadbe61 Compare August 11, 2026 20:59
DIP-14 extends BIP32 child key derivation to 256-bit child indexes with an explicit hardened flag, replacing the high-bit convention. It underpins the DIP-13 Platform key hierarchy and DIP-15 friendship-derived address spaces, where child indexes are 256-bit identity hashes.

Add DIP14Hash, CKey::Derive256, and CPubKey::Derive256. Indexes below 2^32 use the DIP-14 BIP32 compatibility behavior: the hardened flag is folded into the high bit on the private side, while public derivation rejects indexes with that bit set. Invalid index sizes are rejected at the API boundary.

Cover the published DIP-14 vectors, compatibility and public/private consistency, malformed input lengths, and randomized 32-bit-boundary, general 256-bit, hardened, and tweak-add paths in the key fuzz target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m3-background-completion branch 2 times, most recently from 6a0886c to c28ddc8 Compare August 12, 2026 03:53
PastaPastaPasta and others added 17 commits August 11, 2026 23:05
…rive256)

7745331 feat: add DIP-14 256-bit child key derivation (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  [DIP-14](https://github.com/dashpay/dips/blob/master/dip-0014.md) extends BIP32 child key derivation to 256-bit child indexes with an explicit hardened flag. It underpins the DIP-13 platform key hierarchy and DIP-15 friendship-derived address spaces, where child indexes are 256-bit identity hashes so that the derivation path between two identities cannot be ground out by a third party (a 31-bit index space would be brute-forceable by cycling identities).

  Dash Core currently has no DIP-14 primitive. This PR adds it at the `CKey`/`CPubKey` level, extracted from the Platform GUI work in #49 as the first wallet-side prerequisite.

  ## What was done?

  - `DIP14Hash` in `hash.{h,cpp}`: the HMAC-SHA512 with a 256-bit big-endian (`ser256`) child index, alongside `BIP32Hash`.
  - `CKey::Derive256`: private child derivation with a 256-bit index and explicit hardened flag.
  - `CPubKey::Derive256`: the public (non-hardened only) counterpart. Non-hardened 256-bit public derivation matches private derivation, which is what lets a counterparty derive addresses from an exported xpub without seeing private keys.

  **Compatibility-mode semantics** (worth calling out, since the DIP-14 pseudocode is loosely worded here): indexes below 2^32 are interpreted as raw BIP32 index space, per the DIP's Compatibility section ("if a child key's index is less than 2^32 … will match the derivation outlined in BIP32") and its published test vectors. Concretely, the hardened flag is folded into the high bit on the private side, and the public side rejects indexes with the high bit set exactly as BIP32 does. Vector 2 (`m/9'/5'/15'/0'/…`) only reproduces under this interpretation.

  ## How Has This Been Tested?

  New unit suite `dip14_tests`:
  - the four DIP-14 spec test vectors from dashpay/dips dip-0014.md, including the mixed 32-bit/256-bit hardened path;
  - BIP32 compatibility of the sub-2^32 fallback (private hardened/non-hardened and public, checked against `Derive`);
  - public/private derivation consistency over a 256-bit two-step path, and rejection of hardened indexes on the public side.

  `dip14_tests`, `key_tests`, `bip32_tests`, and `hash_tests` pass locally (macOS, clang). `lint-whitespace`, `lint-include-guards`, `lint-includes` clean; the new Dash-specific test file is listed in `test/util/data/non-backported.txt`.

  ## Breaking Changes

  None. Additive API only; no existing derivation path changes behavior.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] 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)

Top commit has no ACKs.

Tree-SHA512: adff8e353e2e9477c6c82da12137f997a187d803aa2b4186b82e03ef280c926afc8ba847bfda494f8817b3e977c902a86a854fb41ec1fd52b46bc926cf1b2cf8
…e Qt lsan suppressions

19a28d8 ci: drop Berkeley DB from the asan job, extend the Qt lsan suppressions (Konstantin Akimov)

Pull request description:

  ## Issue being fixed or feature implemented
  This PR should replace https://github.com/dashpay/dash/pull/7560/changes
  The Linux ASan job exposed two independent dependency-owned lifetime reports:

      Qt's process-global DBus connection manager retains 86 bytes across four allocations at shutdown. The same QDBusConnectionManager::executeConnectionRequest report appears on unrelated develop-based PRs.
      Berkeley DB 4.8 retains DB_PRIVATE lock objects after reopened environments are torn down and memory-pool file metadata after mock databases are closed.

  The previous branch suppressed Berkeley DB's shared __os_malloc allocator and changed Dash wallet environment lifecycle code. The allocator rule was too broad, and the production changes were not the cause of either retained allocation. LeakSanitizer also appends a suppression summary to subprocess stderr, breaking exact stderr checks in wallet_hd.py and tool_wallet.py.

  ## What was done?

  For Berkeley DB, do what upstream did in 04a7a7a and stop building it in this job. Upstream's asan job carried -DWITH_BDB=ON and libdb5.3++-dev until that commit, ran the same tool_wallet.py and wallet_hd.py, and never needed a Berkeley DB entry in test/sanitizer_suppressions/lsan; the file has never contained one. BerkeleyEnvironment::Close() here is identical to the last upstream version, so the difference is not wallet code but the dependency: upstream linked the system libdb 5.3 shared library, while this job takes DEP_OPTS="" and so links 4.8.30 statically out of depends. The msan job is already configured the same way.

  Qt still needs a suppression, and by symbol rather than by module. Upstream suppresses Qt with leak:libQt6Widgets, which only works for a shared library; with Qt linked statically from depends the allocation belongs to the executable and that form cannot match. Upstream hit this in 5be31b2 and answered it with per-symbol rules, so add QDBusConnectionManager next to the two symbols already taken from that commit.

  ## 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 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
  - [x] I have assigned this pull request to a milestone

Top commit has no ACKs.

Tree-SHA512: f098a15f4f44353336bfea7d6d34e1cd94edf45072c90b59aedb6f1db40b0906f41c47635ed2af212ed38b41a4e0bb5995962cf015706c4476c52ee821e0bbdd
The DKG pending-message retention quota was keyed by NodeId, which resets on reconnect: a single masternode identity could retain an unbounded number of pending messages across reconnects. Key the quota by the sender's proTxHash instead, which survives reconnects and is pinned to registered masternode identities by the MNAuth gate (develop already rejects pushed DKG messages from peers without a verified proRegTxHash). The quota is cumulative for the round and not refunded on pop, so draining the queue does not regain retention slots.

Also check the duplicate-hash set before charging the quota so resent hashes don't burn budget, and do not mark a quota-dropped hash as seen so another peer with remaining budget can re-deliver it. Locally produced messages (from=-1) are enqueued under this node's own proTxHash and charged like any other sender's.

Extracted from the DKG intake redesign in dashpay#7557; the deserialize-once/framing changes there are deliberately not included.
Unit tests: the quota is charged per proTxHash (surviving NodeId changes and not refunded on pop), duplicates are rejected before charging the quota, distinct proTxHashes have independent budgets, and locally produced messages share the quota path.

Functional test: extend feature_llmq_dkg_intake.py with a late-message scenario proving that a masternode identity reconnecting under fresh NodeIds cannot retain more than maxMessagesPerProTx contributions, that quota drops are silent (banscore stays 0), that a distinct proTx keeps its own budget, and that round-start clearing discards retained messages without them ever reaching a worker.

Ported from dashpay#7557, adapted to the pre-framing intake (well-formed zero-BLS payloads instead of BLS-invalid ones, since develop still deserializes a copy at intake).
…ompletion and snapshot promotion

c28ddc8 test: cover background MN-hash erase and the promote/discard recovery overlap (pasta)
a8b6790 docs: fix stale check_block_index reference and explain deferred candidate admission (pasta)
d16440b refactor: centralize durable rename/remove for the snapshot lifecycle (pasta)
413afb1 fix: report a missing snapshot base block as a startup error, not an abort (pasta)
4b1294c test: assert cold snapshot activation captures no base MN-list marker (pasta)
cedba66 docs: note the cross-chainstate duplicate-commitment corner (pasta)
ab3b671 validation: move the mempool to the snapshot chainstate on activation (pasta)
44bd797 refactor: share the block-data-unavailable sentinel as a named constant (pasta)
2ef1222 backport: adapt bitcoin#27746 for Dash (Pasta)
43efc47 Merge bitcoin#27746: Rework validation logic for assumeutxo (Ryan Ofsky)
d1ffdd6 Merge bitcoin#28050: test: make assumeUTXO test capture the expected fatal error (Pasta)
3da9105 Merge bitcoin#27862: validation: Stricter assumeutxo error handling when renaming chainstates (Pasta)
9cfc65a backport: adapt Dash EvoDB completion path for bitcoin#25740 (Pasta)
483b3d7 Merge bitcoin#25740: assumeutxo: background validation completion (Pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  [M1 (dashpay#7451)](dashpay#7451) added AssumeUTXO snapshot persistence and [M2 (dashpay#7456)](dashpay#7456) gave the snapshot and background chainstates independent EvoDB identities, markers, and chain-aware Dash validation. What was still missing is the end of the lifecycle: nothing ever *completed* background validation, so a snapshot-backed node stayed in the dual-chainstate state (with DKG participation and quorum signing disabled) forever.

  This is milestone 3 of the AssumeUTXO series: background validation completion. When the background chainstate reaches the snapshot base block, the node now verifies the background-derived state against the snapshot, disables the background chainstate, and on the next restart promotes the snapshot chainstate (coins directory and EvoDB markers) to the normal single-chainstate layout.

  ## What was done?

  Upstream backports (kept 1:1 where practical, Dash adaptations in separate commits):

  - **bitcoin#25740** — assumeutxo: background validation completion. Adds `ChainstateManager::MaybeCompleteSnapshotValidation()` (UTXO-set hash comparison against `m_assumeutxo_data` when the background tip reaches the base block) and `ValidatedSnapshotCleanup()` (restart-time promotion of `chainstate_snapshot` over `chainstate`), with `SnapshotCompletionResult` reporting and the `chainstate_snapshot_INVALID` quarantine path.
  - **bitcoin#27862** — stricter error handling when renaming chainstates (abort instead of continuing after a failed rename).
  - **bitcoin#28050** — test: capture the expected fatal error during assumeutxo completion failure.
  - **bitcoin#27746** — rework validation logic for assumeutxo: `Chainstate::m_disabled` replaces ad-hoc usability checks, `CompleteChainstateInitialization()` split out of `LoadChainstate()` so chainstates can be reinitialized after cleanup, `LoadExternalBlockFile` moved to `ChainstateManager`, per-blockfile undo tracking without the active-chain reference, and `BLOCK_ASSUMED_VALID` documentation/semantics updates.

  Dash-specific completion path:

  - **EvoDB marker promotion.** `CEvoDB::PromoteSnapshotMarkers()` atomically (single synced batch) moves the SNAPSHOT best-block marker to the legacy NORMAL key and removes all dual-chainstate metadata; `DiscardSnapshotMarkers()` does the same for a rejected snapshot while preserving NORMAL state. Both reset the transaction-less default identity to NORMAL, closing the `TODO(assumeutxo)` markers left in M2.
  - **Base-state comparison.** Snapshot activation records a canonical hash of the deterministic masternode list at the base block (`EVODB_SNAPSHOT_MNLIST_HASH`); the background chainstate independently records the list hash it derives when it connects the base block (`EVODB_BACKGROUND_MNLIST_HASH`). Completion compares them (in addition to the upstream UTXO-set hash) and fails with `SnapshotCompletionResult::EVO_STATE_MISMATCH` on divergence. This is the first installment of the holistic base-state comparison M2 deferred; extending it to the CbTx `merkleRootMNList`/`merkleRootQuorums` and credit-pool commitments is called out as a TODO for the `loadtxoutset` milestone, where the snapshot payload gains Dash state. Until then this comparison is a **corruption tripwire, not an independent check**: in the only case both markers exist (activation with the background tip already at the base) they are written from a single derivation, and a cold-start activation captures neither and skips the comparison. Divergence therefore only signals on-disk damage to the marker pair; the independent comparison arrives when the snapshot payload carries the base MN list.
  - **Crash recovery.** `ValidatedSnapshotCleanup()` performs two directory renames plus a marker promotion, each individually durable. `RecoverSnapshotCleanup()` (run at startup before chainstate detection) classifies every interruption point — first rename done, both renames done with markers pending, promotion durable but deletion pending, invalid-snapshot rename done with marker discard pending — and either rolls back, finishes the promotion, or fails with a precise error instead of the generic reindex advice.
  - **Lifecycle correctness fixes discovered while wiring the above:** `EraseSnapshotMarkers()` (the abandoned-activation rollback from M2) now also erases the new MN-list-hash markers; snapshot activation moves the mempool to the snapshot chainstate and restart activation clears it from the background chainstate (the bitcoin#27596 shape), so background block connects can no longer call `removeForBlock`/`removeExpiredAssetUnlock` against mempool state built on the snapshot tip; the invalid-snapshot revert hands the mempool back.

  Review follow-ups from the M2 merge applied here:

  - The peer-penalty exemption for unavailable history no longer depends on three files repeating one literal string: the sentinel is a named constant (`BLOCK_DATA_UNAVAILABLE_SUFFIX`) shared by every producer and the matcher.
  - The background MN-list hash is computed only for the snapshot base block (activation captures it directly when the background tip is already at the base). The initial implementation hashed the full deterministic MN list on every block connect, which would have been a measurable IBD regression on every node.
  - A comment documents the cross-chainstate duplicate-commitment corner in `CQuorumBlockProcessor::ProcessCommitment`.

  Review follow-ups from the dashpay#7553 review round:

  - A snapshot base block missing from the on-disk block index is now reported by `ChainstateManager::LoadBlockIndex()` as a normal startup failure (recoverable via the standard reindex advice), instead of aborting in candidate admission; `GetSnapshotBaseBlock()` regains upstream's cached `SnapshotBase()` delegation (bitcoin d4a11ab) that the initial adaptation had dropped. Note the related deliberate deviation: `MaybeCompleteSnapshotValidation()` converts one upstream hard assert into a `SKIPPED` return for synthetic in-memory unit fixtures, discriminated by `CoinsDB().StoragePath()` being empty.
  - Every snapshot-lifecycle directory rename/removal now goes through new `RenameDurably()`/`RemoveAllDurably()` helpers (`fs::rename`/`fs::remove_all` + `DirectoryCommit`), so the crash-recovery invariant is enforced by the helper rather than by remembering a follow-up call at six sites.
  - New tests: a missing-base startup failure, the promote/discard overlap state in `RecoverSnapshotCleanup` (must land in the same end state as a completed discard), and background-MN-hash coverage in the marker rollback test.

  With completion wired, the M2 duty gate resolves end-to-end: `IsSnapshotActiveAndUnvalidated()` becomes false at completion, so DKG participation and quorum signing re-enable without a restart, and the `masternode status` clause clears.

  ## How Has This Been Tested?

  - Rebased onto develop immediately after the M2 merge (`ab65592f85d`); every conflict was resolved against M2's final review round (thread-scoped EvoDB transactions, `EraseSnapshotMarkers`, reindex-time snapshot discard, fallible `DetectSnapshotChainstate`, BLS scheme establishment). The merged M2 test `chainstate_connectblock_bls_scheme` is adapted in the bitcoin#27746 commit for `AcceptBlock` moving to `ChainstateManager`.
  - Full clean build (autotools, `--enable-debug`), then the complete `test_dash` suite passes ("No errors detected"), including targeted reruns of `evo_db_tests`, `validation_chainstatemanager_tests`, `validation_chainstate_tests`, `evo_deterministicmns_tests`, `evo_mnhf_tests`, `evo_assetlocks_tests`, `evo_cbtx_tests`, `blockmanager_tests`, `coinstatsindex_tests`, and `validation_block_tests`.
  - New coverage: `snapshot_marker_promotion_and_discard` (promotion/discard idempotency across restarts), the extended abandoned-activation marker rollback test, `chainstatemanager_snapshot_completion` and `_hash_mismatch` (upstream-shaped), an `EVO_STATE_MISMATCH` completion case, four crash-recovery tests that each reproduce a distinct `ValidatedSnapshotCleanup` interruption point on disk and drive it through `LoadVerifyActivateChainstate()`, and mempool-ownership assertions at both activation paths.
  - An independent review pass traced the highest-risk interactions end to end: the `ConnectTip` → `MaybeCompleteSnapshotValidation` EvoDB transaction lifecycle (the scoped committer closes before completion runs, so the single-open-transaction invariant holds), BLS-scheme guard nesting across connect/disconnect, all four mempool handoff transitions, and the recovery state machine. Its two "correct but implicit" findings are addressed in the final commit (at-rest raw reads for the lifecycle markers; a comment documenting the deliberate promote/discard overlap in `RecoverSnapshotCleanup`).
  - `lint-circular-dependencies`, `lint-python`, and `git diff --check` are clean.

  ## Breaking Changes

  None released. The dual-chainstate on-disk state introduced in M2 (unreleased) gains two lifecycle marker keys (`b_dcs_mn`, `b_dcs_bg_mn`); nodes that never load a snapshot never write any of them. `ChainstateLoadStatus::FAILURE_FATAL` is a new internal failure class treated like `FAILURE_INCOMPATIBLE_DB` at init.

  ## Checklist:

  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation (`doc/design/assumeutxo.md` updated for the implemented lifecycle; the user-facing AssumeUTXO documentation lands with `loadtxoutset`)
  - [ ] I have assigned this pull request to a milestone

Top commit has no ACKs.

Tree-SHA512: 0a5b28e1d1301a9c7a3bd0d825e2890408baf6d93ef697c8569913f680540122af277f3860386416bdbb5df5d6ee087af8f1f9af8780bd3d7a5967a420fa4896
…ecovery

RecoverSnapshotCleanup re-spelled the snapshot, todelete and INVALID directory names as string literals while the rename sites derived them from SNAPSHOT_CHAINSTATE_SUFFIX or inlined them. A rename of any suffix would compile cleanly while crash recovery silently stopped matching the on-disk layout. Define the INVALID and todelete suffixes next to SNAPSHOT_CHAINSTATE_SUFFIX and consume them at every producer and consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n loop

The candidate-admission pass in ResetBlockFailureFlags called ChainstateManager::GetAll() for every reconsidered block, re-acquiring cs_main recursively and heap-allocating a vector per iteration even though the chainstate set cannot change while cs_main is held for the whole function. Fetch the set once before the loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
assert(snapshot_base_height == index_new.nHeight) restated assert(index_new.nHeight == snapshot_base_height) from four lines earlier; upstream carries only the first assert plus the curr_height check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d4851d8 chore: drop duplicated snapshot base-height assert (pasta)
8cd7f27 refactor: hoist chainstate enumeration out of the flag-reset admission loop (pasta)
04caf09 refactor: share snapshot lifecycle dir suffixes between writers and recovery (pasta)

Pull request description:

  ## Issue being fixed or feature implemented
  Review of the assumeutxo M3 PR (dashpay#7553, now merged) surfaced three small non-blocking cleanups. They were split out so dashpay#7553 could merge as-is:

  1. `RecoverSnapshotCleanup()` re-spelled all four snapshot-lifecycle directory names as string literals while the rename sites derived them from `SNAPSHOT_CHAINSTATE_SUFFIX` or inlined them. A rename of any suffix would compile cleanly while crash recovery silently stopped matching the on-disk layout.
  2. The candidate-admission pass in `ResetBlockFailureFlags()` called `ChainstateManager::GetAll()` inside the per-block loop, re-acquiring `cs_main` recursively and heap-allocating a vector per reconsidered block even though the chainstate set cannot change while `cs_main` is held for the whole function.
  3. `MaybeCompleteSnapshotValidation()` carried `assert(snapshot_base_height == index_new.nHeight)` four lines after asserting the same equality.

  ## What was done?
  - Added `SNAPSHOT_INVALID_SUFFIX` and `SNAPSHOT_TODELETE_SUFFIX` next to `SNAPSHOT_CHAINSTATE_SUFFIX` in `node/utxo_snapshot.h` and consume them in `RecoverSnapshotCleanup()`, `InvalidateCoinsDBOnDisk()`, and `ValidatedSnapshotCleanup()`. Test files intentionally keep spelled-out literals so they would catch an accidental rename of the on-disk names.
  - Hoisted the `GetAll()` call above the admission loop.
  - Dropped the duplicated assert.

  ## How Has This Been Tested?
  Both touched translation units compile with `--enable-werror`. No behavior change is intended; the existing `validation_chainstatemanager_tests` snapshot-recovery cases cover the renamed-directory handling.

  ## Breaking Changes
  None.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [ ] 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)_

Top commit has no ACKs.

Tree-SHA512: c0259abb3cf0d0629c7f0db41c8a0738478fa0562928695b5f88c4a75dcc578f655af60659ee5180ba468ec0f514a4adaba2837ea584a110102b507752f5e516
…xHash

cf85283 test: cover per-proTx DKG pending-message quotas (pasta)
4b7e21f fix: bound pending DKG message retention per proTxHash (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  The DKG pending-message retention quota in `CDKGPendingMessages` is keyed by NodeId, which resets on every reconnect. A single masternode identity can therefore retain an effectively unbounded number of pending DKG messages on a victim masternode by reconnecting between sends.

  This extracts the DoS-relevant part of dashpay#7557 so it can ship in the release; the deserialize-once/framing intake redesign remains in dashpay#7557 for post-RC.

  ## What was done?

  - Rekeyed the retention quota to the sender's MNAuth-verified proTxHash (locally produced messages charge this node's own proTxHash). The quota is cumulative for the round and not refunded on pop, so neither reconnects nor queue drains regain retention slots. The MNAuth gate pins quota keys to registered masternode identities.
  - The duplicate-hash check now runs before the quota charge so resent hashes don't burn budget, and quota-dropped hashes are not marked seen, so another peer with remaining budget can re-deliver the message.
  - `PopAndDeserializeMessages` and the framing/deserialize-once changes from dashpay#7557 are intentionally not included.

  ## How Has This Been Tested?

  - New unit tests in `llmq_dkg_tests`: quota survives NodeId changes, quota is per-proTx, duplicates are rejected before charging the quota, own-message enqueue path shares the quota.
  - Extended `feature_llmq_dkg_intake.py` with a late-message scenario: a reconnecting identity cannot exceed its budget across `2*llmq_size` fresh NodeIds, over-quota drops are silent (banscore 0), a distinct proTx has its own budget, and round-start clearing discards retained messages without scoring.
  - `make -j5`, `test_dash --run_test=llmq_dkg_tests`, `test_runner.py feature_llmq_dkg_intake.py`, `lint-python`, `lint-whitespace`.

  ## Breaking Changes

  None. Honest masternodes send one message per type per round, far under the unchanged `size * 2` budget; the only behavior change is that a reconnecting identity can no longer refill its retention quota.

  ## Checklist:

  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] 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)_

Top commit has no ACKs.

Tree-SHA512: cf881cef9465b8b37f31b9630c937d35c40024ed86771d20d717b4a09a06b9c2467662d05c50a8b4a1f92b96d62dceafe565478eafb6f35610f2f994ef6e2ce0
quorum_members_reconstruct_from_seeded_state_only forced DIP0003/v19/v20 active through const_cast on a TestChain100Setup chain that was mined with those forks inactive. The v20 quorum hash modifier then probed a pre-DIP3 coinbase for a chainlock and tripped GetTxPayload's payload-type assertion in --enable-debug builds. The abort longjmps past fixture teardown, so mocktime, gArgs, and worker threads leaked and poisoned every later case in the same test_dash process.

Mine the fixture chain with the required activation heights already in force instead (new SnapshotActivationChainSetup with its own deterministic checkpoint), and restore const_cast-mutated consensus params through a scope guard so a failed assertion in one case can no longer leak mutated params into cases that run after it.
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m4-evo-snapshot branch from aadbe61 to 3cfd05d Compare August 12, 2026 17:45
BuildNewQuorumQuarterMembers accumulates one skip list across every quorum index of a rotated cycle and can wrap the combined MN list more than once, so params.size does not bound a legitimate list: ReadRotationSnapshot and Validate() would reject a snapshot produced from valid chain state, making dumptxoutset/assumeutxo unavailable at that base. Bound claimed sizes by a dedicated decode ceiling with the upfront reserve clamped to quorum size, and only reject a negative first (absolute) skip entry since later delta entries legitimately go negative after wraparound. Reported by Codex review.

Co-Authored-By: Claude Fable 5 <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.

6 participants