dash(embedded): daemonless superblock payee sourcing via govsync (governance triggers)#810
Conversation
Adversarial review — daemonless superblock payee sourcing (reward-safety focus)Verdict: CHANGES-REQUIRED (draft-appropriate: the fail-closed audit for the current binary state PASSES — see below — but the PR contains latent reward-critical defects in exactly the code the KATs present as proven, plus one false safety-claim comment, and the completeness gate must be encoded as a hard precondition rather than a follow-up note). All dashcore references below verified against dashpay/dash master ( Reward-critical findings (ranked)R1. P2SH superblock payees produce a WRONG scriptPubKey → dashd rejects the block. R2. The govobj object-hash computation does not match dashcore — and the comment claims it does. R3. Trigger funding votes are BLS-operator-signed, not ECDSA/keyIDVoting — the documented follow-up verifier design is the wrong scheme. R4. Vote tally semantics diverge from dashcore once votes flow. R5. THE dangerous case (partial govsync → confidently-wrong winner) is real and the gate must be hard, not a note. R6. No superblock desync cross-check (the analogue of the MN payee-desync latch). Fail-closed audit (question: can flag-ON with stubs unset ever serve?) — PASS, quadruplyTraced every mutation path into
Trigger-selection shape (question 1): dashcore applies the threshold via Parse divergences (accept-more-than-dashd; vote-gated in practice but weakens defense-in-depth)dashcore
Functional / robustness
Pre-existing-failure verificationConfirmed empirically: Required soak scope (the actual gate — this area is 6× soak-refuted)The soak MUST cross a funded superblock with the embedded arm's schedule checked against a dashd-accepted block. Since testnet superblocks are currently unfunded:
Bottom line: default-OFF is genuinely reward-safe today and even flag-ON cannot serve (quadruple fail-closed, verified). But R1 (P2SH mis-script) sits inside the "proven by KAT" core, R2's comment asserts a false safety property, R3 documents the wrong verification scheme for the exact votes being tallied, and R5's gate must be structural. Fix R1/R2-comment/R5-structure in this PR; R3/R4/R6 + F1 are hard preconditions for ever letting the flag serve. |
…ernance triggers)
Close the last daemonless gap for --embedded-mainnet: SUPERBLOCK heights.
Previously the embedded arm unconditionally fell back to dashd at every
superblock height because it could not source the governance-determined
treasury (superblock) payees. This adds the governance-object sync leg over
the existing coin-P2P client and reuses dashcore's superblock consensus logic
to produce the exact (payee, amount) vector daemonlessly, with the same
fail-closed discipline as E1/E2/E3.
Reuse of dashcore (governance/governance-classes.cpp):
- CSuperblock::ParsePaymentSchedule -> governance_object.hpp
parse_superblock_trigger (payment_addresses|payment_amounts split,
address->scriptPubKey, fixed-point amount)
- CSuperblockManager::GetBestSuperblock -> governance_store.hpp
get_best_superblock (funding-tally winner selection)
- CSuperblockManager::GetSuperblockPayments / CalcSuperblockBudget ->
superblock.hpp get_superblock_payments / superblock_budget
govsync leg (mirrors the E1 mnlistdiff sourcing pattern):
- p2p_messages.hpp: govsync (request) / govobj / govobjvote (dashcore
protocol.cpp command strings + object.h/vote.h field layouts)
- p2p_client.hpp: handlers fire new_govobject/new_govvote + send_govsync
- govsync_ingest.hpp + coin_state_maintainer.hpp: GovernanceStore feed
- node_coin_state.hpp / work_source.hpp / embedded_gbt.hpp: superblock
schedule threaded into the coinbase (payees + augmented coinbasevalue)
Fail-closed (never guess superblock payees): a superblock height serves the
embedded arm ONLY when a trigger is funding-threshold-confident AND its
schedule is budget-valid; otherwise it falls back to the reward-safe dashd
path. Opt-in via --embedded-superblock (default OFF preserves prior behaviour
exactly). Vote-ECDSA-verify against keyIDVoting is the documented follow-up:
the verifier seam is UNSET by default so no vote is counted => no trigger wins
=> superblock heights fail closed even with the flag on, until pinned + soaked.
KATs (test_dash_superblock.cpp, 11 tests) pin the reused logic against
FROM-WIRE testnet vectors (dashd @192.168.86.52): ParsePaymentSchedule byte-
exact scriptPubKey match, superblock_budget == getsuperblockbudget
(1428625704 duffs @ h1519824 cycle 24), fail-closed below funding threshold,
over-budget rejection, and build_embedded_workdata superblock-output wiring.
R1 (shared core, reward-critical): core::address_to_script P2SH whitelist lacked DASH's versions — a mainnet '7…' superblock payee (the treasury- multisig class) decoded to a P2PKH 76a914…88ac script instead of P2SH a914…87, a coinbase dashd rejects at every superblock. Added 0x10 (mainnet) + 0x13 (testnet) with a per-coin version-map comment; verified non-colliding with every supported coin's P2PKH bytes and pinned cross-coin byte-exactness (LTC/DOGE/BTC/DASH) in a KAT that fails on the old core. The trigger parser additionally decodes CHAIN-STRICT in the dash module (dashcore DecodeDestination parity): only the active chain's DASH version pair is accepted — wrong-chain / other-coin / bech32 payees fail the trigger closed (dashd-confirmed: '7…' is invalid on testnet). R2: the govobj hash now implements the EXACT dashcore Governance::Object::GetHash preimage (excludes collateralHash+type, HexStr(vchData) string layer, legacy dummy bytes) — pinned byte-exact against two from-wire testnet governance objects. Also fixed the wire vchData handling: dashcore's vchData is the PLAINTEXT JSON bytes (no hex layer on the wire; RPC DataHex is hex OF those bytes) — the ingest no longer hex-decodes it. The false 'matches GetHash' comment is gone. R3: corrected the vote-verify DIRECTION everywhere: TRIGGER funding votes are BLS-signed by the MN's OPERATOR key (dashcore IsValid: useVotingKey only for PROPOSAL funding votes) — the previously documented ECDSA/keyIDVoting contract would verify zero genuine trigger votes. The verifier seam stays UNSET (fail closed); the exact BLS scheme (SetBytes(vchSig,false) / VerifyInsecure over GetSignatureHash), the signed digest (govvote_signature_hash, pinned; identity hash pinned against two from-wire votes), the +1h time bound, and the DMN-lookup requirement are documented at the seam for the follow-up. R4: tally now matches dashcore CountMatchingVotes semantics: per-vote weight seam (EvoNodes 4x, membership-at-tally — 0 for an MN no longer in the valid set; UNSET default = weight 0 = fail closed), and the funding threshold is max(nGovernanceMinQuorum, weighted_count/10) re-derived from the SML's weighted count on EVERY accepted mnlistdiff (and zeroed on SML reorg wipes) instead of a one-shot sml/10+1 seed. The DIP-4 SML lacks collateral outpoints, so the outpoint→weight mapping is an explicit seam pending the full DMN view — documented, fail-closed-unset. R5 (structural): resolve_superblock now additionally requires a govsync-completeness predicate (set_superblock_sync_complete_fn) to be PRESENT and TRUE before the serve path can open — default-absent => refuse => dashd fallback. No production caller sets it; landing vote-verify alone can never serve from a partial governance view (the confidently-wrong-winner hazard). KAT walks the full ladder. R6: superblock desync cross-check on the block-connect leg — at a superblock height, if we were trigger-confident, every (script, amount) we would have served must appear in the accepted block's coinbase; mismatch clears the governance store and LATCHES the superblock arm closed (survives re-ingestion; only a restart unlatches). Executed cycles are pruned (prune_executed now has a caller). Guarded by the superblock-height predicate — cannot false-fire off superblock heights (KATs for fire / no-fire / match+prune). Parser hardening (dashd-reject parity): mandatory type field, integer event_block_height only, mandatory proposal_hashes with count match + 64-hex validation, dashcore-exact ParsePaymentAmount grammar (charset [0-9.], <=20 chars, no leading dot/zeros, single dot, >=1 and <=8 frac digits, MoneyRange), legacy nested-array form rejected outright, trigger store bounded (MAX_TRIGGERS=64). Per-vote logging demoted to DEBUG. The handshake govsync comment now states the leg is inert (inventory-based sync gap) instead of claiming a re-prime that does not exist. test_dash_superblock: 24 KATs (was 11), incl. P2SH byte-exact vectors (dashd validateaddress-confirmed), the R1 core regression KAT (fails-before/passes-after), govobject/govvote hash pins against from-wire dashd data, weighted-tally/threshold KATs, the R5 gate ladder, and the R6 latch suite. c2pool-dash + embedded_gbt(30)/get_work(4)/ subsidy(8)/address suites all green; the only failure remains the pre-existing PayeeDesyncWipesDemotesAndFiresReseed #807 residual (reproduces at merge-base). Fail-closed invariant preserved: with --embedded-superblock ON and all seams unset, zero superblock templates are served — now five independent layers deep (inert sync leg, weight seam unset, verifier unset, threshold 0 until gov params + SML, R5 gate absent).
…ck_connected The #802 body<->header trust-boundary guard (block_body_binds_to_header) in CoinStateMaintainer::on_block_connected REFUSES any block whose tx set does not fold to the header merkle commitment. The PayeeDesyncWipes... KAT built its coinbase-only block but never bound it, so the guard returned early and the desync/wipe/demote/reseed path never ran -> EXPECT_TRUE(r.payee_desync) failed. Mirrors the correct usage at the adjacent normal-block KAT (line 279). Test-only, +1 line.
a355369 to
ccbd6a1
Compare
The E-SUPERBLOCK KATs (test_dash_superblock, 24 tests #1529-1552) were registered via gtest_add_tests AUTO but the target was absent from the build.yml --target allowlists for both the Linux x86_64 and ASan+UBSan jobs, so the executable was never compiled -> NOT_BUILT sentinel -> Unable to find executable -> all 24 Not Run -> ctest exit 8. The test lives in test/CMakeLists.txt, outside the src/impl/dash/test scan path of check_test_target_allowlist.py, so the drift-guard did not flag it.
What / why
Closes the last daemonless gap for
--embedded-mainnet: superblock heights. The DASH embedded arm builds templates daemonlessly for every height class (CbTx byte-proven, DKG-window type-6 quorum commitments synthesized per E1 #803, credit-pool per E2, SML/quorum persistence per E3) except superblock heights, where the coinbase must pay the governance-determined treasury (superblock) payees. The E4 soak confirmed superblock 1519464 fell back to dashd reward-safe by design. This PR sources those payees daemonlessly.Draft — do NOT merge. Awaiting re-review + soak. The daemonless-serve path is opt-in (
--embedded-superblock, default OFF) and fails closed until BLS vote-verify + the govsync-completeness proof land (see "Stubbed / follow-up").Rev 2 — reworked per the adversarial review (R1–R6 + parser hardening):
core::address_to_script's P2SH whitelist gained DASH's versions 0x10 (mainnet '7…') / 0x13 (testnet) — before, a mainnet P2SH superblock payee (the treasury-multisig class) built a P2PKH script dashd rejects. Verified non-colliding with every supported coin's P2PKH bytes; cross-coin byte-exactness (LTC/DOGE/BTC/DASH) pinned in a KAT that fails on the old core. The trigger parser additionally decodes chain-strict in the dash module (DecodeDestination parity): wrong-chain / other-coin / bech32 payees fail the trigger closed.Governance::Object::GetHashpreimage (excludes collateralHash+type, HexStr(vchData) string layer, legacy dummy bytes) — pinned byte-exact against two from-wire testnet objects. WirevchDatais now correctly treated as plaintext JSON bytes (no hex layer on the wire).IsValid: the ECDSA/keyIDVoting path is proposal-only). The verifier seam stays UNSET (fail closed); the exact BLS scheme, signed digest (govvote_signature_hash, pinned; identity hash pinned against two from-wire votes), time bound, and DMN-lookup requirement are documented at the seam.max(nGovernanceMinQuorum, weighted_count/10)re-derived from the SML's weighted count on every accepted mnlistdiff (zeroed on SML reorg).resolve_superblockadditionally requires a govsync-completeness predicate (set_superblock_sync_complete_fn) present AND true — default-absent ⇒ refuse ⇒ dashd fallback. No production caller sets it, so landing vote-verify alone can never open the serve path from a partial governance view.type, integerevent_block_heightonly, mandatoryproposal_hashes(count match + 64-hex each), dashcore-exactParsePaymentAmountgrammar (no+/-, ≤20 chars, no leading dot/zeros, single dot, ≤8 frac digits, MoneyRange), legacy nested-array form rejected, trigger store bounded (64), per-vote logging demoted to DEBUG.Dash Core reuse (mostly-reuse mandate)
CSuperblock::ParsePaymentSchedule+ParsePaymentAmount(governance/classes.cpp)governance_object.hpp::parse_superblock_trigger/parse_payment_amountGovernance::Object::GetHash(governance/common.cpp)governance_object.hpp::govobject_hashCGovernanceVote::GetSignatureHash/GetHash(governance/vote.h)govvote_signature_hash/govvote_identity_hashCGovernanceObject::CountMatchingVotes+UpdateSentinelVariablesthresholdGovernanceStoreweighted tally +governance_funding_thresholdCSuperblockManager::GetBestSuperblock(funding-tally winner)governance_store.hpp::get_best_superblockCSuperblockManager::GetSuperblockPayments/CalcSuperblockBudgetsuperblock.hpp::get_superblock_payments/superblock_budgetCGovernanceObject/CGovernanceVotewire, commandsgovsync/govobj/govobjvotep2p_messages.hppgovsync leg (mirrors the E1 mnlistdiff sourcing pattern)
p2p_messages.hpp:govsync(request) /govobj/govobjvote.p2p_client.hpp: handlers firenew_govobject/new_govvotewith dashcore-exact digests;send_govsync()primed at handshake.govsync_ingest.hpp+coin_state_maintainer.hpp: feed the in-memoryGovernanceStore(cycle-ephemeral; a restart re-syncs).node_coin_state.hpp/work_source.hpp/embedded_gbt.hpp: the winning trigger's schedule is threaded into the coinbase (appended payees + augmentedcoinbasevalue, matching dashd GBT).Known gap (pre-enable requirement, documented at the call site): dashcore answers
govsyncwith inventory (gov invs), not directgovobj/govobjvotemessages, and our inv handler does not getdata governance types — the ingest leg is currently inert (an extra fail-closed layer: the store stays empty). Inv-driven getdata + per-object vote sync + periodic re-prime co-land with vote-verify.Fail-closed (never guess superblock payees)
With
--embedded-superblockON and all seams unset, zero superblock templates are served — five independent layers deep:resolve_superblock).set_require_superblock_provider(false)(the default) preserves prior behaviour byte-for-byte (every superblock height falls back).KATs —
test_dash_superblock.cpp(24 tests, all pass)Pinned against from-wire testnet vectors (dashd @192.168.86.52):
ParsePaymentSchedule→ byte-exact scriptPubKey vsvalidateaddress, incl. P2SH (8xXa…→a914…87, dashd-confirmed) and the mainnet '7…' treasury class; chain-strict wrong-chain rejection (dashd-confirmed "Invalid prefix").core::address_to_script('7…'/'8…')→a914…87; fails on the pre-fix core; LTC/DOGE/BTC vectors byte-unchanged.govobject_hashbyte-exact vs twogobject listobjects;govvote_identity_hashbyte-exact vs twogobject getcurrentvotesvotes;govvote_signature_hashstructurally pinned.superblock_budget(1519824, 24)==getsuperblockbudget== 1428625704 duffs (exact).getgovernanceinfo(17); store bound + prune.build_embedded_workdataemits the superblock output + augments coinbasevalue.Stubbed / follow-up (documented, reward-safe by construction)
set_vote_verifier, unset): scheme + digest + time bound + DMN-lookup requirement documented at the seam; requires the bls-signatures lib and a from-wire vote + operator-key vector pin. The outpoint→MN weight mapping (set_vote_weight_fn, unset) needs the full DMN view — the DIP-4 SML lacks collateral outpoints.Testing
c2pool-dashfull binary builds clean (GCC 13, Debug).test_dash_superblock24/24;test_dash_embedded_gbt30/30,test_dash_get_work4/4,test_dash_subsidy8/8 unchanged; address suites (test_address_resolution39,test_redistribute_address12,test_multiaddress_pplns31) green against the R1 core change.test_dash_coin_state_maintainer.PayeeDesyncWipesDemotesAndFiresReseedreproduces on cleanorigin/master(not introduced here).