hasDataChanged() only compared fresh data against CachedStakingDetails -
which stays empty until a submission matures and gets finalized. For any
account whose real stake was genuinely stable, every 5-minute scan still
saw "changed from empty cache", resubmitted, and reset submitted_at to the
current block. Confirmed live: Serok's and QaziM's real, correctly-computed
stake (490,000 / 100,000 HEZ) never once reached CachedStakingDetails/
TrustScores despite multiple successful submissions - each new scan arrived
before the previous one's 300-block dispute window elapsed and overwrote it
with a fresh timer, a self-perpetuating loop.
getPendingData() now reads PendingStakingDetails first; hasDataChanged()
compares against that when present, only falling back to the cache once
nothing is pending. Applied to both the AssetHub and legacy RelayChain-clear
comparisons. Also reordered main()'s cycle to finalizeMaturedPending()
before fullScan(), so anything that matured this cycle gets promoted before
a new scan pass runs at all.
The staking-score pallet's audit-fix (commit 9afd99ae) added a dispute-window
model: receive_staking_details() only writes to PendingStakingDetails, and a
separate, permissionless finalize_staking_details() call promotes it to
CachedStakingDetails once DisputeWindow blocks have passed. No caller of that
promotion step existed anywhere in this bot (or anywhere else in this repo) -
confirmed live: Serok's and QaziM's real stake was correctly submitted to
Pending after the earlier NotRegisteredNoter/nonce fixes, but sat there
indefinitely with no path to ever reaching CachedStakingDetails/TrustScores.
Adds finalizeMaturedPending(), called once per periodic scan cycle (and once
at startup): scans PendingStakingDetails.entries(), finalizes whichever have
matured, batched into one utility.batchAll like the existing submission path,
routed through the same submitQueue so it can never race with a concurrent
submitStakingDetails call.
fullScan() processes accounts in concurrent batches of 10, and each account's
own data-gathering takes variable time, so multiple accounts' submitStakingDetails
calls landed close enough together to race on nonce assignment - the pool then
rejected most of them as duplicate-priority ("Priority is too low ... already in
the pool"). Observed live: 37 of 50 tracked accounts silently dropped in a single
scan cycle, including real accounts with genuine stake (Serok, QaziM) whose trust
scores stayed stuck at 0 as a result even after the separate NotRegisteredNoter
bond issue was fixed.
Routes every submission (both fullScan's batches and the live event listener)
through a single queue so exactly one signAndSend is ever in flight at a time,
in call order - data-gathering stays concurrent (unaffected), only the actual
chain submission is serialized.
processAccount() was comparing direct staking against cache BEFORE
checking pool data. This caused a flip-flop: when cache=121K and
direct=100K, the bot submitted 100K (missing pool). Next scan it
detected the difference and submitted 121K. Every 5 minutes, forever.
Worse, when direct staking went to 0, the bot submitted 0 which
caused the pallet to delete StakingStartBlock — permanently removing
the user from tracking and zeroing their trust score.
Fix: collect ALL staking data (direct + pool) first, combine into
a single total, then compare against cache. Also adds queryFailed
flag to prevent downgrading stake when pool RPC query fails.