Skip to content

dash(replay-fold): gate payee cross-check on DIP3 ENFORCEMENT (fix h=1028163 self-derive divergence) [DRAFT] - #1274

Closed
frstrtr wants to merge 1 commit into
dash/uaf-coldbridge-replayfold-store-exclusionfrom
dash/dip3-payee-tiebreak-fold
Closed

dash(replay-fold): gate payee cross-check on DIP3 ENFORCEMENT (fix h=1028163 self-derive divergence) [DRAFT]#1274
frstrtr wants to merge 1 commit into
dash/uaf-coldbridge-replayfold-store-exclusionfrom
dash/dip3-payee-tiebreak-fold

Conversation

@frstrtr

@frstrtr frstrtr commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Port (B) — payee-ordering fold-correctness (DIP3-genesis regime)

Stacked on #1270 (dash/uaf-coldbridge-replayfold-store-exclusion). DRAFT, do not merge.

Root cause (NOT the tie-break)

The full-DIP3 self-derive replay poisoned at h=1028163 (3rd DIP3 block, 14 fresh MNs) with DML FOLD PAYEE MISMATCH even though merkleRootMNList MATCHED (computed==committed). The MN SET was right; the payee cross-check was wrong to run there.

I audited the tie-break the task fingered and it is already dashd-exact:

  • payee_before() = memcmp(a.data(), b.data(), 32) < 0 == dashd uint256 operator< (base_blob::Compare, uint256.h:55/59) — the storage-byte order, not the base_uint arithmetic order that bit Tier-2 (dash(mempool): tx-serving block order — dashd GetHash() memcmp tie-break (ordering residual → 0) #1271).
  • compare_by_last_paid_height / nRegisteredHeight / pass-5 bookkeeping all byte-match dashd CompareByLastPaid_GetHeight / RebuildListFromBlock.
  • The SML-root match at the divergence height also proves the proTxHash byte layout is dashd-correct (it commits proRegTxHash).

The real bug: DIP3 has two heights. The deterministic list is built from ACTIVATION (1028160) but the coinbase paying GetMNPayee(list) is only ENFORCED from DIP0003EnforcementHeight (mainnet 1047200). dashd's own consensus rule CMNPaymentsProcessor::IsTransactionValid (masternode/payments.cpp:114) early-returns valid without checking the det payee when !DeploymentDIP0003Enforced(h) (deploymentstatus.h:53). In [activation, enforcement) the historical coinbase pays the legacy masternode winner, not the deterministic projection — so the cross-check is stricter than dashd there and false-poisons a byte-correct fold.

Fix (dashd-faithful, reward-safe)

Gate the payee cross-check on DeploymentDIP0003Enforced(H) (H >= dip0003_enforcement_height, default 1047200), mirroring dashd exactly.

  • The merkleRootMNList SET self-check stays UNCONDITIONAL.
  • The payee projection (pass 0) + nLastPaidHeight bookkeeping (pass 5) also stay unconditional — exactly as dashd BuildNewListFromBlock runs them from activation — so the payee axis is fully derived and self-consistent across the window and correct the instant enforcement begins.
  • Production mines at tip (~2.5M), far above enforcement → money-path check UNCHANGED. No block is ever skipped; the fold now folds through the DIP3-genesis window instead of hard-stopping.

KAT (red → green), BLS=REAL

test_dash_replay_fold.cpp :: DashReplayFoldPreEnforcement — same state + same payment-block (coinbase pays nobody):

  • AtEnforcementPayeeMismatchStillPoisons — H≥enforcement → poison (strict where dashd enforces).
  • BelowEnforcementPayeeMismatchFoldsThrough — H<enforcement → folds through, SET self-check still binds, nLastPaidHeight still bumped.
  • BelowEnforcementPayeePaidIsNotFlaggedSkipped — control.

Verified RED (forcing the old unconditional check poisons at h=102 with the exact merkleRootMNList self-check PASSED … PAYEE MISMATCH message — the h=1028163 signature) → GREEN (gated check folds through). Full test_dash_mn_state fold+prestate suites: 31/31 pass, no regressions.

…t activation

The DML fold's coinbase payee cross-check fired from DIP3 ACTIVATION
(h=1028160), but dashd only ENFORCES the coinbase paying GetMNPayee(list)
from DIP0003EnforcementHeight (mainnet 1047200). dashd's own consensus
rule CMNPaymentsProcessor::IsTransactionValid (masternode/payments.cpp:114)
early-returns valid WITHOUT checking the det payee when
!DeploymentDIP0003Enforced(nBlockHeight); deploymentstatus.h:52 spells out
that DIP3 'active' and 'enforced' are deliberately different statuses. In
the [activation, enforcement) window the historical coinbase pays the
LEGACY masternode winner, not the deterministic projection, so a cross-check
there is STRICTER THAN DASHD and false-poisons a byte-correct fold.

Observed in the full-DIP3 self-derive replay: h=1028163 (the 3rd DIP3
block, 14 fresh MNs) poisoned with DML FOLD PAYEE MISMATCH even though
merkleRootMNList MATCHED (computed==committed) — the MN SET was right, the
projection was sound, the coinbase simply paid the legacy winner. The tie-
break itself was already dashd-exact: payee_before() is memcmp over the
proTxHash bytes == dashd uint256 operator< (base_blob::Compare), NOT the
base_uint arithmetic order that bit Tier-2 (#1271); the SML-root match at
the divergence height also proves the proTxHash byte layout is correct. So
the fix is the enforcement gate, not the comparison.

Mirror dashd exactly: run the payee cross-check only when
DeploymentDIP0003Enforced(H) (H >= dip0003_enforcement_height, default
1047200). The merkleRootMNList SET self-check stays UNCONDITIONAL; the
payee projection (pass 0) and nLastPaidHeight bookkeeping (pass 5) also run
unconditionally, exactly as dashd BuildNewListFromBlock does from
activation, so the payee axis stays fully derived and self-consistent
across the window and is correct the instant enforcement begins. Production
mines at tip (~2.5M), far above enforcement, so the money-path check is
UNCHANGED — reward-safe.

KAT (test_dash_replay_fold.cpp, DashReplayFoldPreEnforcement): the same
state + same payment-block (coinbase pays nobody) POISONS at/above
enforcement (strict where dashd enforces) and FOLDS THROUGH below it, with
the SET self-check binding in both arms. Verified red (unconditional check
poisons at h=102) -> green (gated check folds through). BLS=REAL.
@frstrtr

frstrtr commented Aug 20, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #1293 (combined fetch+fold onion). This branch's head commit is present in #1293 by exact SHA (verified via triage). Closing to reconcile the draft backlog; the branch is preserved (not deleted) — reopen if #1293 changes course.

@frstrtr frstrtr closed this Aug 20, 2026
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.

1 participant