dash(replay): early-era (pre-V20) non-rotated quorum members — resolve the h=1081595 LLMQ_50_60 fold poison - #1277
Closed
frstrtr wants to merge 1 commit into
Conversation
…so the fold resolves a REAL LLMQ_50_60 commitment's PoSe punishes
The self-derive full-DIP3 fold folded 1028161..1081594 byte-clean
(folded=53434, roots_matched=53434) and then failed CLOSED at h=1081595
on the FIRST non-null early LLMQ_50_60 (llmqType=1) qfcommit:
fold FAILED at h=1081595 tx[1]: quorum-member resolver has no member
set for llmqType=1 quorumHash=0000000000000001... — failing closed
Root cause is NOT commitment_is_null(): it is already dashd
CFinalCommitment::IsNull-faithful and the h=1081595 commitment is
genuinely non-null (41/50 signers, real pubkey/vvecHash/sigs) — a real
DKG era (mainnet's first mined LLMQ_50_60, May 2019) whose 9
invalid-marked members dashd's HandleQuorumCommitment PoSe-punishes.
Resolving its member set is required for MN-list parity.
The gap is member PRODUCTION below the V20 floor. QuorumReplayEngine::
observe_block() refuses every block under v20_floor (mainnet 1'987'776)
— the CL modifier era, quarter-rotation and the merkleRootQuorums
self-check are all post-V20 / Phase-2 — so the W4 lane produced NO
member set the whole early-DIP3 run (quorum_lane_refusals counted one
per block) and the W1 fold's punish pass fell to the fail-closed site.
Fix: wire dashd's pre-V20 non-rotated ComputeQuorumMembers as a
below-floor PRODUCER. For a non-rotated DKG base block B (B % dkgInterval
== 0) below the floor, the member set is (dashpay/dash llmq/utils.cpp,
verified against develop):
* member list = GetListForBlock(B) — the list AS OF the base block,
NO -8 work-block offset (the offset lives only in GetHashModifier's
ChainLock lookup; ComputeQuorumMembers feeds the base-block list).
Sourced as the W1 fold's just-proven DML at B (post_fold), which IS
GetListForBlock(B) by construction.
* modifier = ::SerializeHash(make_pair(llmqType, blockHash(B))) —
GetHashModifier's pre-V20 branch (DeploymentActiveAfter(..,V20) is
false), no ChainLock term. Byte-exact compute_quorum_modifier's
CL-absent branch fed the BASE hash.
* selection = CalculateQuorum(size, modifier) over confirmed+valid
MNs (compute_nonrotated_members / eligible() == CalculateScores).
Keyed by (llmqType, blockHash(B)) via the same m_height_by_hash the
forward path uses, so members_for(type, quorumHash) resolves a
commitment whose quorumHash == its DKG base block hash. Iterates the
CHAINPARAMS llmq list (still carries LLMQ_50_60, mined in this era), not
the runtime-enabled set. STRICTLY below the floor — at/above it
observe_block() stays authoritative and is never shadowed. Wired into
the bridge's post_fold hook (both the replay-fold and mn-diff-store
drivers). commitment_is_null() is UNCHANGED: a genuinely null early
commitment still skips before any member set is consulted.
Reward-safe by construction: a WRONG member set re-hashes to the wrong
merkleRootMNList and the fold's per-block committed-cbTx-root self-check
refuses forward — never a bad mint.
KAT (test_dash_replay_quorum_seam, red->green):
* EarlyNonRotatedGateRefusesWithoutTheProducer — the poison gate:
below the floor observe_block refuses and members_for is nullopt.
* EarlyNonRotatedProducerResolvesMembersFaithfully — GREEN: the
producer resolves the LLMQ_50_60 set, byte-identical to dashd's
pre-V20 ComputeQuorumMembers, banned MNs excluded, and the pre-V20
modifier is distinct from the post-V20 (work-block-hash) form.
* EarlyProducerIsANoOpAtOrAboveTheV20Floor — the below-floor scope.
127-test suite green (1 data-driven skip).
This was referenced Aug 18, 2026
Owner
Author
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.
Draft. Do not merge. Stacked on #1276 (
integrator/dashd-cut-header-backfill-canserve).The poison (live-proven)
The daemonless full-DIP3 self-derive fold (
dash-selfderive-hdrfix.log) folded 1028161..1081594 byte-clean (folded=53434, roots_matched=53434, payees_verified=34395, DIVERGED=none) — #1274 payee-DIP3 enforcement proven — then failed CLOSED at h=1081595 on the FIRST non-null earlyLLMQ_50_60(llmqType=1) qfcommit:quorum_lane_refusals=53435(every block).Root cause (NOT commitment_is_null)
commitment_is_null()is already dashdCFinalCommitment::IsNull-faithful and unchanged. The h=1081595 commitment is genuinely non-null (41/50 signers, real quorumPublicKey / quorumVvecHash / sigs) — mainnet's first minedLLMQ_50_60DKG era (May 2019) with 9 invalid members that dashd'sHandleQuorumCommitmentPoSe-punishes. A member set is required for MN-list parity.The gap is member production below the V20 floor.
QuorumReplayEngine::observe_block()hard-refuses every block underv20_floor(mainnet 1'987'776) — the CL-modifier era, DIP-0024 rotation and themerkleRootQuorumsself-check are all post-V20 / Phase-2 — so the W4 lane produced no member set the whole early-DIP3 run and the W1 fold's punish pass fell to the fail-closed site.Fix — faithful dashd pre-V20
ComputeQuorumMembers(verified vs dashpay/dashdevelop)For a non-rotated DKG base block B (
B % dkgInterval == 0) below the floor:GetListForBlock(B)— the list AS OF the base block, NO -8 work-block offset (the offset lives only inGetHashModifier's ChainLock lookup;ComputeQuorumMembersfeeds the base-block list). Sourced as the W1 fold's just-proven DML at B (post_fold) =GetListForBlock(B)by construction.::SerializeHash(make_pair(llmqType, blockHash(B)))—GetHashModifier's pre-V20 branch (DeploymentActiveAfter(pWorkBlockIndex, V20)is false), no ChainLock term. Byte-exactcompute_quorum_modifier's CL-absent branch fed the BASE hash.CalculateQuorum(size, modifier)over confirmed+valid MNs (compute_nonrotated_members/eligible()== dashdCalculateScores).Keyed by
(llmqType, blockHash(B))via the samem_height_by_hashthe forward path uses, somembers_for(type, quorumHash)resolves a commitment whose quorumHash == its DKG base block hash. Iterates the chainparams llmq list (still carriesLLMQ_50_60, mined in this era), not the runtime-enabled set. Strictly below the floor — at/above itobserve_block()stays authoritative and is never shadowed. Wired into the bridge's post_fold hook (covers both the replay-fold and mn-diff-store drivers).Reward-safe by construction: a wrong member set re-hashes to the wrong
merkleRootMNListand the fold's per-block committed-cbTx-root self-check refuses forward — never a bad mint.Tests (red→green)
test_dash_replay_quorum_seam:EarlyNonRotatedGateRefusesWithoutTheProducer— the poison gate: below the floorobserve_blockrefuses andmembers_foris nullopt.EarlyNonRotatedProducerResolvesMembersFaithfully— GREEN: the producer resolves theLLMQ_50_60set, byte-identical to dashd's pre-V20ComputeQuorumMembers, banned MNs excluded, and the pre-V20 modifier is distinct from the post-V20 (work-block-hash) form.EarlyProducerIsANoOpAtOrAboveTheV20Floor— the below-floor scope.test_dash_mn_statesuite: 126 passed, 1 data-driven skip. Build BLS=REAL.Live validation
Binary rebuilt; the self-derive run resumed from the existing capture (re-folds DIP3→1081595 from cached bodies). Acceptance: fold PAST h=1081595 with
DIVERGED=nonecontinuing and members resolving. (Full run to tip is ~days; not awaited here.)