mirror of
https://github.com/Such-Software/grin-lws.git
synced 2026-10-17 15:41:43 +02:00
fix(scanner): advance only the accounts a tick actually scanned
An account that registered while the initial full scan was still running got marked caught-up without ever being scanned, then stayed at a 0 balance forever (and the backend synced-gate would trust that 0). The advance step used a blanket UPDATE-by-height; on the first tick prev_tip is 0, so it matched any account, including one registered mid-scan. Advance only the caught_up set snapshotted before the scan; an account not in it falls through to the backfill phase on a later tick. Found by live-node validation against a funded wallet.
This commit is contained in:
@@ -117,22 +117,10 @@ pub async fn list_accounts(pool: &AnyPool) -> Result<Vec<AccountRow>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Advance the scan height of already-caught-up accounts (scan_height at/above
|
||||
/// the previous tip) to the new tip after a forward scan pass.
|
||||
pub async fn advance_caught_up_scan_heights(
|
||||
pool: &AnyPool,
|
||||
to_height: i64,
|
||||
from_height: i64,
|
||||
) -> Result<()> {
|
||||
sqlx::query("UPDATE accounts SET scan_height = $1 WHERE scan_height >= $2")
|
||||
.bind(to_height)
|
||||
.bind(from_height)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set one account's scan height (used after its backfill completes).
|
||||
/// Set one account's scan height (after a forward-scan pass covers it, or after
|
||||
/// its backfill completes). Advancing per-account (rather than a blanket
|
||||
/// UPDATE-by-height) is deliberate: it must never mark an account that was not
|
||||
/// actually scanned as caught-up.
|
||||
pub async fn set_account_scan_height(pool: &AnyPool, rewind_hash: &str, height: i64) -> Result<()> {
|
||||
sqlx::query("UPDATE accounts SET scan_height = $1 WHERE rewind_hash = $2")
|
||||
.bind(height)
|
||||
|
||||
+11
-2
@@ -100,10 +100,19 @@ async fn tick(pool: &AnyPool, node: &GrinNode) -> crate::error::Result<()> {
|
||||
// ── Phase 2: spend reconcile (by absence from the unspent set) ──────────
|
||||
reconcile_spends(pool, node, tip_height).await?;
|
||||
|
||||
// ── Phase 3: advance the cursor + caught-up accounts ────────────────────
|
||||
// ── Phase 3: advance the cursor + the accounts we actually scanned ──────
|
||||
// (Only after discovery AND reconcile succeeded — Risk R1.)
|
||||
db::set_cursor(pool, new_mmr_index as i64, tip_height as i64, &tip_hash).await?;
|
||||
db::advance_caught_up_scan_heights(pool, tip_height as i64, prev_tip as i64).await?;
|
||||
// Advance ONLY the accounts that were in this tick's `caught_up` set — i.e.
|
||||
// the ones the forward scan above actually covered. A blanket
|
||||
// UPDATE-by-height would also mark an account that REGISTERED DURING this
|
||||
// tick's scan (so it was never in `caught_up`, never scanned) as caught-up,
|
||||
// leaving it stuck at a 0 balance forever — and the backend's synced gate
|
||||
// would then trust that 0. Such an account instead stays "behind" and is
|
||||
// picked up by the backfill phase on a later tick.
|
||||
for rh in &caught_up {
|
||||
db::set_account_scan_height(pool, rh, tip_height as i64).await?;
|
||||
}
|
||||
|
||||
// ── Phase 4: backfill one pending new account (birthday → cursor) ───────
|
||||
if let Some(acct) = accounts.iter().find(|a| a.scan_height < prev_tip) {
|
||||
|
||||
Reference in New Issue
Block a user