feat: evo snapshot v3 — canonical bounded codec and context-free validation - #7592
Draft
PastaPastaPasta wants to merge 3 commits into
Draft
feat: evo snapshot v3 — canonical bounded codec and context-free validation#7592PastaPastaPasta wants to merge 3 commits into
PastaPastaPasta wants to merge 3 commits into
Conversation
This was referenced Aug 13, 2026
…e validation First code PR of the assumeutxo M4 series (dashpay#7579 decomposition): the versioned interchange format for Dash's evo state alongside a UTXO snapshot - canonical serialization, DoS-bounded validating decode, and every validation invariant that needs no chain context. Chain-aware building/validation and dump/load integration follow in the next PRs of the series. Canonical ordering exists because snapshot content is hashed and cross-checked; per-object serializers are reused through a bounded stream wrapper, with bespoke code only at container level (ordering, bounds, per-entry budgets); decode-time checks deliberately stay out of the trusted hot EvoDB deserializers. AssumeutxoData gains the EvoSnapshotHash anchor the format is pinned by. Includes the aggregate rotation skip-list bound (lists accumulate across every quorum index and wrap the combined MN list), the CRangesSet bounded unserializer, and a vendored-immer shift-base ubsan suppression reachable only through the deliberately hash-colliding test fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sign change The mask for rejecting out-of-range trailing bits promotes through operator~ to a negative int before its implicit conversion back to uint8_t, which clang's implicit-integer-sign-change check reports for every bitset whose size is not a multiple of eight. The evo snapshot unit tests are the first to deserialize such bitsets under the sanitizer job. Same bits, stated explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PastaPastaPasta
force-pushed
the
feat/evo-snapshot-format
branch
from
August 13, 2026 05:13
3779689 to
3398d9a
Compare
The codec PR shipped UnserializeBounded without its unit coverage; add the malformed/canonical decode matrix and the round-trip checks from the original series. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Issue being fixed or feature implemented
Part of the AssumeUTXO M4 decomposition (#7579, now draft — see the series map there). For a Dash node, a UTXO snapshot alone is not enough to operate at the base block: the node also needs the deterministic MN list, quorum commitments, rotation state, credit pool, and MNHF signals that consensus at that height depends on. This PR defines the evo snapshot v3 format: the versioned interchange encoding for that state, its DoS-hardened decoder, and every validation invariant that needs no chain context. It deliberately contains no chain access and no lifecycle wiring — building a snapshot from chain state and validating one against the chain come in the next PR of the series;
dumptxoutset/load integration after that. Reviewing this PR is reviewing the wire format and its trust boundary, nothing else.What was done?
src/evo/snapshot.{h,cpp}: theCEvoSnapshottypes, canonical serialization, bounded validating deserialization, andCEvoSnapshot::Validate()(context-free invariants), plusReconstructHistoricalMNLists(),CanonicalMNListHash(),GetEvoSnapshotHash(), andVerifyEvoSnapshotCbTx()(pure CbTx cross-checks over decoded content).AssumeutxoDatagains anEvoSnapshotHashfield: the hard-coded expected hash of the canonical evo section, the same security anchor rolehash_serializedplays for the UTXO set.CDeterministicMNListgainsApplyDiffForSnapshot()andGetHeightForSnapshotCodec();CRangesSetgains a bounded validating unserializer;OverrideStreamgainsGetStream()for the per-object decode budgets.shift-base), reachable only through the unit tests' deliberately hash-colliding MN fixtures, in the style of the existing vendored-library entries.ReadFixedBitSetwhose trailing-bits mask otherwise trips clang's implicit-sign-change check for any bitset size not a multiple of eight — these tests are the first to decode such bitsets under the sanitizer job.Why a bespoke codec instead of the classes' own serializers (raised in #7579 review): (1) snapshot content is hashed and cross-checked (the completion-time MN-list comparison and CbTx checks), so the encoding must be a pure function of set content — hence canonical proTxHash ordering rather than container iteration order; (2) the snapshot file is untrusted by definition and read once, so its decoder validates and bounds everything, while the EvoDB/P2P deserializers are trusted hot paths that would pay that tax per block; (3) a versioned interchange format must not silently drift when in-memory serialization changes. Note per-object serializers are reused —
CDeterministicMN, commitments, and the credit pool decode through their ownSERIALIZE_METHODSwrapped in a budgeted stream; only the container level (ordering, bounds, budgets) is bespoke.Open question for reviewers (from #7579 feedback on header surface): the codec helpers are
Stream-templated and therefore header-bound; I can move them into anevo::detailnamespace to shrink the nominal API if preferred — say the word and it's a small mechanical commit.How Has This Been Tested?
Full unit suite on a
--enable-werrorbuild, plus the snapshot/netinfo/util suites under a--with-sanitizers=undefined,integerbuild with the repo's ubsan suppressions (which is what surfaced theReadFixedBitSetand immer items above).Breaking Changes
None. The format is new and nothing constructs or consumes it on-chain yet; the
AssumeutxoDatafield is populated with a null placeholder for the existing regtest entries.Checklist: