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
- Enable Silent Payments on a Bitcoin wallet whose
restoreHeight is well below tip (pre-taproot or early post-taproot is enough).
- Let the scan run until the UI has clearly moved (thousands of blocks).
- Force-quit the app (do not wait for “synced”).
- 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.
Describe the bug
Silent Payments scan progress shown in the UI (
SyncingSyncStatus.fromHeightValues(..., tweakHeight)) is not the height written toWalletInfo.restoreHeight. After killing/restarting the app mid-scan,blockchain.tweaks.subscribeis 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 everySyncResponse, includingSyncingSyncStatus. The isolate sends the wrong height in that message.Code
In
_handleScanSilentPayments/listenFn(cw_bitcoin/lib/electrum_wallet.dart):tweakHeightis the height map’s last key (the block just processed).syncHeightat send time is still the subscribe start, then the previous event’s last key.The main isolate always persists that field:
(
updateRestoreHeightisrestoreHeight = height; await save();incw_core/lib/wallet_info.dart.)So for one long subscribe from restore height
H:tweakHeight)SyncResponse.height)HHHK)KHKA 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
Hagain.{"message":"done"}is handled asnoDataand only resubscribes fromsyncHeight + 1if still behind tip. It does not callupdateRestoreHeight. Cake also requestscount = 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. Overlappingsave()s can complete out of order, so an older start height can win even after later events.Expected behavior
restoreHeightshould 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
scanOutputsfor that map succeeds:Optionally serialize persist (await the previous
updateRestoreHeightbefore starting the next) so SQLite cannot reorder writes.To Reproduce
restoreHeightis well below tip (pre-taproot or early post-taproot is enough).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.subscribestream (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, butrestoreHeightstays at the subscribe start until the next event.Happy to test a patch.