Skip to content

Silent Payments scan persist lags one notify (restoreHeight stays at subscribe start after restart) #3574

Description

@rearden-grok

Describe the bug

Silent Payments scan progress shown in the UI (SyncingSyncStatus.fromHeightValues(..., tweakHeight)) is not the height written to WalletInfo.restoreHeight. After killing/restarting the app mid-scan, blockchain.tweaks.subscribe is issued from the original restore height (or one event behind), not from the last height the isolate actually finished.

This is not gated on the server sending {"message":"done"}. Progress is written on every SyncResponse, including SyncingSyncStatus. The isolate sends the wrong height in that message.

Code

In _handleScanSilentPayments / listenFn (cw_bitcoin/lib/electrum_wallet.dart):

final tweakHeight = response.block;

final syncingStatus = /* ... fromHeightValues(..., tweakHeight) */;

if (shouldUpdateSyncStatus) scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// … ECDH / scanOutputs …
syncHeight = tweakHeight;

tweakHeight is the height map’s last key (the block just processed). syncHeight at send time is still the subscribe start, then the previous event’s last key.

The main isolate always persists that field:

if (message is SyncResponse) {
  // … UI status …
  await walletInfo.updateRestoreHeight(message.height);
}

(updateRestoreHeight is restoreHeight = height; await save(); in cw_core/lib/wallet_info.dart.)

So for one long subscribe from restore height H:

Stream event UI (tweakHeight) Persisted (SyncResponse.height)
RPC result for H H H
Next notify (last key K) K H
Notify after that new last key previous K

A restart anywhere in the first two events (or during ECDH of the first fat height, which is still event 1) starts the next subscribe at H again.

{"message":"done"} is handled as noData and only resubscribes from syncHeight + 1 if still behind tip. It does not call updateRestoreHeight. Cake also requests count = tip − start + 1, so a server that honors that in one RPC never takes this path until the scan is finished.

Extra: un-awaited SQLite saves

receivePort.listen((var message) async { … await walletInfo.updateRestoreHeight(…) }) does not await the callback. Overlapping save()s can complete out of order, so an older start height can win even after later events.

Expected behavior

restoreHeight should be the last height whose tweaks were fully scanned, so a restart continues from there.

Suggested fix

Send the height that was just processed, and only after scanOutputs for that map succeeds:

syncHeight = tweakHeight;
if (shouldUpdateSyncStatus) {
  scanData.sendPort.send(SyncResponse(tweakHeight, syncingStatus));
}

Optionally serialize persist (await the previous updateRestoreHeight before starting the next) so SQLite cannot reorder writes.

To Reproduce

  1. Enable Silent Payments on a Bitcoin wallet whose restoreHeight is well below tip (pre-taproot or early post-taproot is enough).
  2. Let the scan run until the UI has clearly moved (thousands of blocks).
  3. Force-quit the app (do not wait for “synced”).
  4. Reopen the same wallet and start Silent Payments scan again.

Actual: subscribe start is the original restore height (or one notify behind).
Expected: subscribe start is the last height shown as scanned.

Additional context

Seen against an Electrum server that implements Cake’s blockchain.tweaks.subscribe stream (JSON-RPC result = first height, remaining heights as notifications, {"message":"done"} at the end of the requested range). The same persist lag is visible if the server collapses many empty pre-taproot heights into one notify: the UI jumps by the last key, but restoreHeight stays at the subscribe start until the next event.

Happy to test a patch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions