镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(scanner): point-in-time PoS reads and typed replay errors - #2152

Merged
nullPointerEnjoyer merged 6 commits into
mintlayer:masterfrom
nullPointerEnjoyer:fix/scanner-rescan-staker-drift
Oct 31, 2026
Merged

nullPointerEnjoyer merged 6 commits into
mintlayer:masterfrom
nullPointerEnjoyer:fix/scanner-rescan-staker-drift

Conversation

@nullPointerEnjoyer

Copy link
Copy Markdown
Collaborator

Re-creation of #2149 (head was re-created on top of current master after the dependency and CI fixes landed via #2148; the old PR could not be reopened).

Problem

The scanner's consensus-state reads are not point-in-time. update_tables_from_consensus_data reads pool data and delegation shares as the latest rows in storage, not the state as of the block being connected. When a replay (rescan, partial restore, crash-loop restart) reconnects blocks below surviving future rows, PoS rewards are credited on top of future state. On mainnet this produced a permanent silent drift for the affected pool: an 18x block-subsidy pile-up at a single height, then stretches of +0 staker credits, leaving the scanner ~1600 ML ahead of chain truth. The same latest-row reads in the delegation spend path then let a legitimate on-chain spend underflow the drifted delegation balance, and Delegation::spend_share panicked (not underflow), crash-looping the scanner.

Fix

  1. Point-in-time consensus reads: new storage methods get_pool_data_at_or_below_height, get_delegation_at_or_below_height, get_pool_delegations_at_or_below_height (postgres reads history tables bounded by block height, deliberately bypassing the listing caches; in-memory reads the per-height maps bounded the same way). The consensus, delegation-spend, and delegate-transactions paths now read state as of the block being connected. A replay below future rows can no longer observe them, and catches up instead of piling rewards onto the future.
  2. Typed errors: Delegation::spend_share and PoolDataWithExtraInfo::decrease_delegation_balance return Result instead of panicking on underflow; failures propagate as BlockchainStateError::PosAccountingError (scanners stay up and surface an error instead of crash-looping).
  3. Regression tests: a replay test proving a scanner connecting blocks below an inflated future pool row converges to the clean balance (fails with a +inflation drift without the fix), plus typed-error tests for the spend/decrease paths.
  4. Remaining latest-row reads that could observe future state during replay are bounded the same way, and the scanner storage version is bumped (28) so existing deployments rescan once with correct reads.

Test plan

  • New drift regression test is red/green verified: reverts to latest-row reads produce exactly the injected-inflation drift; with the fix it converges.
  • cargo test -p api-server-common -p api-blockchain-scanner-lib green, including new tests; postgres containerized backend suite green (8/8, including new point-in-time read tests).
  • ./do_checks.sh steps (fmt, cargo deny, cargo vet, both clippy passes, codecheck.py, wasm-doc check) pass locally.
  • Full workspace cargo test green; wasm job flow (wasm-pack build, tsc, node tests, knip) green locally.

@nullPointerEnjoyer
nullPointerEnjoyer merged commit c3009f9 into mintlayer:master Oct 31, 2026
34 of 35 checks passed
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