Skip to content

Check that Core holds the chain-locked block before reading Core at the chain-locked height #5685

Closed as not planned

Description

@shumkov

Context

From protocol version 15, update_core_info v1 (#5681) records the chain-locked height of the first block that sees Dash Core's V24 active. V24's signal is the payload version of the coinbase of Core block c at the block's chain-locked height. It is read through CoreRPCLike::get_coinbase_payload_version(c), which resolves c with getblockhash(c) on the node's own Core. The protocol 14 withdrawal scan (get_credit_pool_balance) and the operator-key logic (#5461) read the coinbase the same way, by height.

The lookup is bound to a height, not to the block the chain lock signs. A Core that holds a different block at c would answer about that block.

Why it is not urgent

  • V24's activation at c is decided through EHF and versionbits a full window before c, so any shallow competing block at c carries the same coinbase payload version.
  • The record is covered by the app hash. A Core that answers about another block makes only that node diverge, at the block that records V24, rather than silently.

Proposal

  • In blocks that carry a chain lock update, read the coinbase by the chain lock's block hash: one getspecialtxes call bound to the quorum-signed block, instead of getblockhash(c) + getspecialtxes.
  • Decide what init chain does: it has no chain lock, and it reads at the genesis core height.
  • Consider the same binding for the other consensus reads of the coinbase by height.

Raised in the review of #5681: #5681 (comment)

Activity

  1. added this to the v5.1.0 milestone on Oct 30, 2026
  2. shumkov commented on Nov 3, 2026

    @shumkov
    CollaboratorAuthor

    Closing: the lookup by height cannot return a block other than the chain-locked one, so there is nothing to bind.

    The record is only written in a block that moves the chain-locked core height, and such a block carries a chain lock update. Before update_core_info runs, a node that did not propose the block:

    1. verifies the proposal's chain lock and submits it to its own Core (run_block_proposal v0);
    2. continues only if Core's best chain-locked height is at least the chain lock's height (make_sure_core_is_synced_to_chain_lock v0), waiting briefly when Core is almost there (verify_chain_lock v0);
    3. otherwise refuses the proposal with ChainLockedBlockNotKnownByCore (run_block_proposal v0), before update_core_info (L269).

    A Core whose best chain lock is at or above c has the chain-locked block at c on its active chain, and Core never reorganizes a chain-locked block. So getblockhash(c) (coinbase_with_payload) returns the same block on every node that gets this far. The proposer's chain lock comes from its own Core, and init chain reads at the genesis core height, deep in the chain.

    The same reasoning covers the other consensus reads of Core by chain-locked height: the masternode list diffs, the quorums and the credit pool balance (get_credit_pool_balance).

    🤖 Generated with Claude Code

  3. shumkov commented on Nov 4, 2026

    @shumkov
    CollaboratorAuthor

    Reopening: my closing comment was wrong.

    Dash Core records a chain lock as its best one even when it does not have the signed block yet (Chainlocks::UpdateBestChainlock in src/chainlock/chainlock.cpp: "We don't know the block/header for this CLSIG yet ... when the block/header later comes in, we will enforce the correct chain"), and submitchainlock returns that best chain lock's height. So make_sure_core_is_synced_to_chain_lock can report Core as synced while Core's active chain does not yet hold the chain-locked block at c. In that window getblockhash(c) either fails (Core has no block at c yet) or returns a block on a competing branch, whose coinbase the node then reads; the node then diverges at the app hash.

    This is not specific to the Core fork record: every consensus read of Core by chain-locked height has the same window (masternode list diffs, quorums, the credit pool balance, the fork record).

    Re-scoping this issue to close the window once for all of them: before any read at c, require that Core's block at c is the chain lock's block (getblockhash(c) == chain_lock.block_hash), and otherwise treat Core as not synced (wait, as the almost-synced path already does, or refuse the proposal). The proposer's own chain lock needs the same check, since getbestchainlock can also return a chain lock whose block is unknown (known_block: false). A spec comes first, since this touches chain lock verification.

    🤖 Generated with Claude Code

  4. reopened this on Nov 4, 2026
  5. changed the title [-]Read the coinbase for Core fork activation by the chain lock's block hash[/-] [+]Check that Core holds the chain-locked block before reading Core at the chain-locked height[/+] on Nov 4, 2026
  6. shumkov commented on Nov 5, 2026

    @shumkov
    CollaboratorAuthor

    Superseded by #5773: instead of an extra hash check over RPC, drive-abci will take chain locks from Core over ZMQ. Core publishes its chain lock notification only after the locked block is on its active chain, which closes the window described here. The analysis above stays as the background for #5773.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions