Skip to content

[Bug] Reconcile validators on every block - #89

Merged
jorgecuesta merged 4 commits into
mainfrom
fix/validator-reconcile-every-block
Aug 3, 2026
Merged

[Bug] Reconcile validators on every block#89
jorgecuesta merged 4 commits into
mainfrom
fix/validator-reconcile-every-block

Conversation

@jorgecuesta

Copy link
Copy Markdown
Collaborator

Summary

The validator chain re-sync ran behind a trigger list that could never fire, so validators were re-synced exactly once per validator creation. It now runs on every block, with a dirty check so an unchanged validator does not open a historical row.

Issue

msgsByType/eventsByType only carry types registered in MsgHandlers/EventHandlers; everything else is dropped into unhandledMsgTypes. Of the six staking messages in VALIDATOR_RECONCILE_MSG_TYPES only MsgCreateValidator has a handler, and none of the three event types do, so hasStakeChange evaluated undefined?.length ?? 0 for 8 of 9 triggers.

On pnf mainnet the last full validator write was block 801233 — 62k blocks behind the tip. NodesHub sat at Unstaked / 90 POKT against bonded / 6,099,991 POKT on chain, PNF-12 at Unstaking against bonded, and 11 of 36 live rows disagreed with the chain. This is what surfaced on the explorer and got reported by PNF.

Reading the raw block would not have been enough either: the staking end-blocker moves validators in and out of the active set with no message at all, and that is exactly the transition that froze both rows.

Applications are unaffected — all 14 of their trigger types are registered, and a height-pinned dry run at 863553 showed 125/125 active apps matching the chain exactly.

Type of change

  • Bug fix

Testing

No unit tests in this repo for the mapping layer. Verified:

  • yarn run lint — 0 errors.
  • jsonFieldEquals exercised standalone over 9 cases: key order, "" vs absent, null vs absent, real moniker change, commission equal/changed, field added, nil/nil, nil/value.
  • Simulated the new path against live data — 28 chain validators vs the 36 live rows in pnf mainnet: it would write the 11 drifted rows and skip the other 17.
  • Full tsc was not run: subql codegen does not emit src/types/proto-interfaces/ in this environment, so build.sh aborts there. Pre-existing, identical before and after this branch; the real build runs in CI.

Sanity Checklist

  • I have tested my changes using the available tooling
  • I have commented my code
  • I have performed a self-review of my own code; both comments & source code

The trigger list never fired: msgByType/eventByType only carry types that have
an entry in MsgHandlers/EventHandlers, and of the six staking messages listed
only MsgCreateValidator has one, so validators were re-synced exactly once per
validator creation — on pnf mainnet the last full write was block 801233, 62k
blocks behind the tip, leaving NodesHub stuck at Unstaked/90 POKT and PNF-12 at
Unstaking. Reading the raw block would not fix it either, since the staking
end-blocker moves validators in and out of the active set with no message at
all, so the trigger list is dropped and the chain re-sync now runs every block;
reconcileValidators skips the rows whose values did not change so the steady
state stays one ABCI read and zero writes.
Pinning the chain read to the block height makes it the one part of block
indexing that depends on the node still serving state at that height, so a
pruned height during a from-genesis reindex or a transient RPC error would throw
out of the block handler and stall indexing; running every block is what makes
catching it safe, since the next block reconciles the same set. Also reports the
"Staked in store but missing from chain" close-out refusal once per validator
per process instead of once per block, and documents the flat-object invariant
jsonFieldEquals depends on.
Wrapping the whole reconcile swallowed bulkCreate failures too, and those write
through the block's Postgres transaction shared with every other handler, so an
aborted write would surface later as an unrelated error in a sibling handler
while the log claimed it was safely retried next block. The tolerance now covers
only the height-pinned chain read, which is the part that legitimately fails on
a pruned height or a transient RPC error. The missing-from-chain report also
forgets a validator once the chain returns it again, so a later incident on the
same id still reports instead of being silenced for the life of the process.
@jorgecuesta jorgecuesta self-assigned this Aug 3, 2026
@jorgecuesta
jorgecuesta merged commit 96f338a into main Aug 3, 2026
3 checks passed
@jorgecuesta
jorgecuesta deleted the fix/validator-reconcile-every-block branch August 3, 2026 16:51
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.

2 participants