Repository navigation
fix(scanner): point-in-time PoS reads and typed replay errors - #2149
Open
nullPointerEnjoyer wants to merge 6 commits into
Open
nullPointerEnjoyer wants to merge 6 commits into
nullPointerEnjoyer wants to merge 6 commits into
Conversation
Fee calculation and pool decommission now read pool state as of the connected height, and the point-in-time delegation shares lookup uses a lateral join over the latest-state cache with primary-key history lookups instead of scanning the delegation history of the pool.
nullPointerEnjoyer
requested review from
ImplOfAnImpl,
anyxem and
erubboli
as code owners
October 28, 2026 21:32
Collaborator
Author
|
CI context: |
Collaborator
Author
|
Note: the 4 failing checks on this PR are inherited from master and are fixed by two separate small PRs: #2150 (dependency bumps for the cargo-deny advisories behind |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When the scanner reconnects blocks below rows that survived in its history tables, its pool and delegation reads return the latest row for the entity — including rows written by higher (future) blocks. Rewards then get credited on top of future state and the accumulated balances silently drift from the chain on every reconnection of a lower block.
Observed on mainnet 1.4.3: a scanner that reconnected from an earlier common ancestor accumulated duplicated block rewards into a staking pool balance (a +17×subsidy lump at a single height, then frozen balances until the drift burned off), which in turn under-accrued delegation shares. The first on-chain spend exceeding the under-accrued delegation balance hit
.expect("not underflow")inDelegation::spend_shareand panic-looped the daemon.Root cause
update_tables_from_consensus_data,prefetch_pool_data, the delegation spend/delegate paths and the decommission helper all read pool/delegation state without an upper height bound, so a replay whose disconnect step failed to purge higher rows (interrupted scan, partial restore) reads state from the future. The panic is the downstream symptom: PoS arithmetic in the scanner storage layer panicked instead of failing typed.Fix
hare now bounded at-or-below reads (get_pool_data_at_or_below_height,get_delegation_at_or_below_height,get_pool_delegations_at_or_below_height), implemented for both postgres and in-memory backends (no default trait methods, so every backend implements them). The postgres bounded delegation-shares query scans the id set from the latest-state cache and resolves each row with a primary-key point lookup in the history table (no new index or schema change). Replaying any range now reproduces the same history as a clean scan.Delegation::spend_shareandPoolDataWithExtraInfo::decrease_delegation_balancereturnResult<_, PosError>instead of panicking on underflow; the scanner propagates them via a newBlockchainStateError::PosAccountingErrorvariant, so a corrupt-balance spend fails the scan instead of crash-looping the daemon.Test plan
connecting_block_ignores_future_pool_rows: connects blocks against a clean replay and a replay seeded with a future pool row, asserts identical results (fails with+10_000coins on the unbounded reads — verified red/green).spend_share/decrease_delegation_balanceunderflow → typed error, and exact-balance spend bumping the nonce.point_in_time_pool_and_delegation_readscovering pool/delegation/shares reads at-or-below heights plus rollback visibility, run against both in-memory (115/115) and containerized postgres (8/8) suites.cargo check --workspace --testsand clippy clean on touched crates.