You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Check that Core holds the chain-locked block before reading Core at the chain-locked height #5685
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.
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:
verifies the proposal's chain lock and submits it to its own Core (run_block_proposal v0);
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).
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.
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
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.
Activity
shumkov commentedon Nov 3, 2026
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_inforuns, a node that did not propose the block:ChainLockedBlockNotKnownByCore(run_block_proposal v0), beforeupdate_core_info(L269).A Core whose best chain lock is at or above
chas the chain-locked block atcon its active chain, and Core never reorganizes a chain-locked block. Sogetblockhash(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
shumkov commentedon Nov 4, 2026
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::UpdateBestChainlockinsrc/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"), andsubmitchainlockreturns that best chain lock's height. Somake_sure_core_is_synced_to_chain_lockcan report Core as synced while Core's active chain does not yet hold the chain-locked block atc. In that windowgetblockhash(c)either fails (Core has no block atcyet) 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 atcis 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, sincegetbestchainlockcan 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
[-]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[/+]shumkov commentedon Nov 5, 2026
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.