Repository navigation
Conversation
When a block is processed again after a restart, the row it wrote the first time is still in the pool table. PoolStatusProcessor looked up the latest REGISTRATION of the pool up to the current epoch, found that row, and saved the registration again as an UPDATE (amount 0, active epoch +3). The pool's next certificate then became the REGISTRATION, so adapot counted the 500 ADA deposit in the wrong epoch. Look only at registrations that come before the current certificate (slot, tx index, cert index), so a replay gets the same answer as the first pass. Found by the fault-injection re-test (#1328). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Initial automated review · Aster Hi @satran004, thanks for the PR! 🙏 This is an automatic first check by an AI reviewer. It does not replace maintainer review and may be wrong. Reviewed commit: No issues found in this first check. Not tested: I did not run the build, tests, or a live chain replay. |
…itEvent PoolStatusProcessor caches pool registration events until pre-commit and publishes the number of REGISTRATION rows in StakingDepositEvent. When a batch failed before pre-commit, the cache kept its events. A block processed again then sat in the cache twice. Now that a replayed registration stays a registration, both copies were counted: 2 in the event, 1 row in the database. - A block processed again replaces its earlier cached event (same slot and block hash), so each certificate is counted once. - A rollback clears all four caches. A restart publishes a rollback to the cursor before it processes blocks again, so events of the failed batch, including blocks the node no longer serves, are not counted or saved at the next pre-commit. This also resets the stake key registration counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
…PoolRegistrationBefore PoolStorage can be replaced by a custom bean. A provider compiled against the previous interface does not have the new method and failed with AbstractMethodError at the first pool registration. The method now has a default that throws UnsupportedOperationException, naming the class and the method to implement. It does not fall back to findRecentPoolRegistration, because that query sees the certificate's own row and would turn a replayed registration into an update again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
…er block PoolRegistrationProcessor publishes one PoolRegistrationEvent per transaction. Replacing a cached event by block dropped the other transactions of the same block, so their pool status rows were not written and the registration count was too low. Replace only the copy of the same transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
|
|
Updated after an external review of the 2.0.x backport #1347: 15f7a3d, 132f4fb, 1e85144. Each finding, its verdict and its test are in the reply on #1339. |
|
Verdict: APPROVED reviewer1 — original-PR review at F1 and F2 from #1347 are resolved. The changed files are identical to the approved backport: transaction-level cache replacement prevents duplicate counts, rollback clears pending caches, and custom PoolStorage providers get an explicit implementation requirement instead of an incorrect fallback. The numbered findings refer to the counterpart review of #1347. No additional blocking finding remains. Validation on combined main: core 244 passed, 1 skipped at |
|
Fault-injection rerun on For this PR:
The baseline is unpatched |



Part of #1328 (fault-injection / auto-recovery fixes). Found in the re-test with all fix PRs.
Problem
yaci-store processes blocks at least once: after a restart it processes blocks again from the cursor, and the rows they wrote the first time can still be in the database.
PoolStatusProcessor.handlePoolRegistrationdecides between REGISTRATION and UPDATE by looking up the pool's latest REGISTRATION row up to the current epoch. When the block is processed again, that lookup finds the row the block itself wrote the first time. The registration is then saved again (same primary key) as an UPDATE: amount 0, and the active epoch is +3 instead of +2. The pool's next certificate then finds no registration and becomes the REGISTRATION.Seen on preprod after scenario D10 (Postgres killed during the initial sync), pool
7cc911f0…:Adapot's
DepositSnapshotServicesumspool.amountper epoch, so the 500 ADA deposit is counted in epoch 153 instead of 148. Nothing is logged.Impact
Deposits: adapot's
DepositSnapshotServicesumspool.amountper epoch, so the 500 ADA deposit is counted in the wrong (later) epoch.Stake and rewards:
StakeSnapshotServicedrops delegations made before the pool'sregistration_slot. The replay movesregistration_slotforward (in the example, from 62368918 to 64580884). So delegations to the pool made between those two slots, and never moved, are left out ofepoch_stake. That makes these wrong:This follows from the code. It was not measured, because adapot was not enabled on the test instances.
Pool API: the first registration shows as an UPDATE, and the active epochs are off by one.
There is no error or warning. Only a resync, or a comparison with another source, shows it.
How often it can happen
It needs both of these:
At the tip: usually only the last block is processed again. On preprod there was about 1 such certificate per epoch over the last 20 epochs, and an epoch has about 21,600 blocks. So a crash at the tip hits this very rarely.
During the initial sync: a restart can process a whole batch again (100 blocks by default, with several batches running in parallel). Preprod has 711 such certificates in about 5.4 million blocks, about 1 in 7,500 blocks. So a crash during the initial sync hits one roughly 1–5% of the time, and more often in busy epochs (preprod epoch 28 has 44). On mainnet, the first Shelley epochs (208 onward) had far more pool registrations, so a crash there is much more likely to hit it.
In our test it happened once in 43 initial-sync fault scenarios.
Fix
The lookup now only considers registrations that come before the current certificate in chain order (slot, then tx index, then cert index). A replayed block gets the same answer as the first time.
Tests
PoolStatusProcessorReplayIT(H2), 5 tests. Without the fix, the 2 replay tests fail.StakingDepositEvent.All 32 staking tests pass. Reviewed by Codex and a review agent; neither found anything that must change.
Not in this PR
DRepStatusProcessor,findRecentByDRepIdAndEpoch). Replaying a deregistration can pick up a later re-registration in the same epoch.🤖 Generated with Claude Code