Skip to content

dash(replay-fold): mirror GetBlockTxOuts operator-reward split in the payee cross-check (h=1439234 sub-class) - #1284

Closed
frstrtr wants to merge 1 commit into
integrator/dashd-cut-early-quorum-membersetfrom
integrator/dashd-cut-fold-operator-full-reward
Closed

dash(replay-fold): mirror GetBlockTxOuts operator-reward split in the payee cross-check (h=1439234 sub-class)#1284
frstrtr wants to merge 1 commit into
integrator/dashd-cut-early-quorum-membersetfrom
integrator/dashd-cut-fold-operator-full-reward

Conversation

@frstrtr

@frstrtr frstrtr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What

Port dashd's operator-reward split into the DML replay fold's pass-6 payee
cross-check so the projected payee matches dashd at h=1439234 and for the
whole event sub-class.

Why (live poison)

The daemonless self-derive hard-stopped:

[REPLAY-PAYEE] G1 fold POISONED: DML FOLD PAYEE MISMATCH at h=1439234 --
this block's coinbase does not pay the projected masternode
71ed3bf5...ceb2d006 -- the merkleRootMNList self-check PASSED at this height

The projection was correct — dashd paid the very MN c2pool projected. The
check was blind to the DIP3 operator-reward split:

  • proTxHash 71ed3bf5...ceb2d006, registered h=1434598, nOperatorReward=0x2710 = 10000 bps = 100.00%, owner==operator self-host.
  • dashd masternode/payments.cpp GetBlockTxOuts:64-77: at bps=10000 the
    operator reward eats the whole MN share, masternodeReward folds to exactly
    0, so the ONLY MN output emitted (and required by IsTransactionValid:109-139)
    is scriptOperatorPayout. scriptPayout is legitimately absent.
  • Block 1439234 paid the operator script for the full 1.51818084 with no owner
    remainder; roots matched 411073/411073. The fold demanded scriptPayout and
    false-poisoned a byte-correct fold.

This recurs for every 100%-operator-reward payment (~one queue length apart
while registered), so a point patch would re-wedge — the general dashd-faithful
branch is required.

The fix (replay_fold_engine.hpp)

  • Pass 0b now captures the payee's full pre-block tuple
    (scriptPayout, scriptOperatorPayout, nOperatorReward) — state already held by
    fold_proreg/fold_proupserv.
  • Pass 6 mirrors GetBlockTxOuts without needing amounts:
    • bps==10000 && operator-script set → require scriptOperatorPayout, not
      scriptPayout (dashd emits none);
    • 0<bps<10000masternodeReward = mnShare - floor(mnShare*bps/10000) is
      provably >0, so require scriptPayout (unchanged); the operator output's
      exact amount needs the fee reward the fold cannot price (W5 gap), so never
      poison on the operator axis for a partial split;
    • otherwise → require scriptPayout (unchanged).
  • The poison message now dumps {scriptPayout, scriptOperatorPayout, bps} so any
    future instance self-classifies.

Passes 0/5 (projection + nLastPaidHeight bookkeeping) are untouched — they were
correct; the projected proTxHash equals the paid MN here.

Reward-safe by construction

Check-only change on the verification axis. The requirement becomes exactly
dashd's required-output set, never weaker: at bps=10000 a coinbase paying
scriptPayout instead of the operator output still hard-stops (dashd would
reject it too). The merkleRootMNList SET self-check stays unconditional; the
payee projection + nLastPaidHeight bookkeeping stay derived. No live-mint
behaviour changes.

Tests (DashReplayFoldOperatorSplit, red→green proven)

  • FullOperatorRewardPaysOperatorScriptFoldsThrough — the h=1439234 shape; RED
    on the old selection (poisons a byte-correct fold), GREEN with the faithful
    branch.
  • FullOperatorRewardPayingOwnerScriptStillPoisons — strictness: a bps=10000
    coinbase paying the owner script is invalid and still poisons.
  • PartialOperatorRewardRequiresOwnerScript / ...MissingOwnerScriptPoisons
    partial splits keep requiring scriptPayout, unchanged.

Build / verification

  • -DC2POOL_DASH_BLS=ON (real dashbls, relic-backed) — c2pool-dash self-reports
    at init: DASH BLS backend: REAL (dashbls linked; quorum-commitment verify armed).
  • test_dash_mn_state: 130 passed, 1 skipped (data-driven), 0 failed.

Stacked on integrator/dashd-cut-early-quorum-memberset (#1277 branch). DRAFT
— do not merge.

… payee cross-check

The DML fold's pass-6 payee cross-check unconditionally required the
projected masternode's scriptPayout among the coinbase outputs. dashd
(masternode/payments.cpp GetBlockTxOuts:64-77) does not always emit it:
when nOperatorReward==10000 and scriptOperatorPayout is set, the operator
reward eats the entire MN share, masternodeReward folds to exactly 0, and
the ONLY MN output dashd emits — and its IsTransactionValid:109-139
requires — is scriptOperatorPayout. Demanding scriptPayout there is
stricter than dashd and false-poisons a byte-correct fold.

Live self-derive incident: h=1439234, proTxHash
71ed3bf59baa91d914ed93b6534cb8e2819f230167b6228cf77b9e03ceb2d006,
bps=10000, owner==operator self-host. The coinbase paid the operator
script for the full 1.51818084 share with no owner remainder; the
merkleRootMNList self-check PASSED (411073/411073), yet the fold hard-
stopped "engine poisoned, re-seed required". This recurs for every
100%-operator-reward payment (~every queue length while registered), so a
point patch would re-wedge; the general dashd-faithful branch is required.

Pass 0b now captures the payee's full pre-block payment tuple
(scriptPayout, scriptOperatorPayout, nOperatorReward — state already held
by fold_proreg/fold_proupserv). Pass 6 mirrors GetBlockTxOuts without
needing amounts:
  - bps==10000 && operator-script set -> require scriptOperatorPayout, NOT
    scriptPayout (dashd emits none).
  - 0<bps<10000 -> masternodeReward = mnShare - floor(mnShare*bps/10000)
    is provably >0, so require scriptPayout (today's behaviour); the
    operator output's exact amount needs the fee reward the fold cannot
    price (W5 gap), so never poison on the operator axis for a partial
    split.
  - otherwise -> require scriptPayout.
The poison message now dumps {scriptPayout, scriptOperatorPayout, bps} so
any future instance self-classifies.

Reward-safe by construction: a check-only change on the verification axis.
The requirement becomes EXACTLY dashd's required-output set, never weaker
— at bps=10000 a coinbase paying scriptPayout instead still hard-stops
(dashd would reject it too). The merkleRootMNList SET self-check and the
payee projection + nLastPaidHeight bookkeeping (passes 0/5) are untouched;
no live-mint behaviour changes.

KATs (test_dash_replay_fold.cpp, DashReplayFoldOperatorSplit):
  - FullOperatorRewardPaysOperatorScriptFoldsThrough — the h=1439234 shape;
    RED on the old selection (poisons a byte-correct fold), GREEN with the
    faithful branch.
  - FullOperatorRewardPayingOwnerScriptStillPoisons — strictness: a bps=10000
    coinbase paying the owner script is invalid and still poisons.
  - PartialOperatorRewardRequiresOwnerScript / ...MissingOwnerScriptPoisons —
    partial splits keep requiring scriptPayout, unchanged.
@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