wallet: Ensure best block matches wallet scan state #30221

pull achow101 wants to merge 10 commits into bitcoin:master from achow101:wallet-no-chainstateflushed changing 16 files +190 −154
  1. achow101 commented at 9:58 pm on June 3, 2024: member

    Implements the idea discussed in #29652 (comment)

    Currently, m_last_block_processed and m_last_block_processed_height are not guaranteed to match the block locator stored in the wallet, nor do either of those fields actually represent the last block that the wallet is synced up to. This is confusing and unintuitive.

    This PR changes the those last block fields to actually be in sync with the record stored on disk. This requires adding the block height to the BESTBLOCK_NOMERKLE record, which was done in a backwards compatible manner. Additionally, the issue with chainStateFlushed fixed in #29652 is also fixed here by removing chainStateFlushed entirely. The last block fields, now simply the best block, is updated for each blockConnected and blockDisconnected so that it does actually represent the block for which the wallet is synced up to.

    To make it easier to deal with, the best block fields are consolidated into a BestBlock struct which also has the serialization code with backwards compatibility.

  2. DrahtBot commented at 9:58 pm on June 3, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/30221.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK fjahr

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31296 (wallet: Translate [default wallet] string in progress messages by ryanofsky)
    • #31250 (wallet: Disable creating and loading legacy wallets by achow101)
    • #31061 (refactor: Check translatable format strings at compile-time by maflcko)
    • #30909 (wallet, assumeutxo: Don’t Assume m_chain_tx_count, Improve wallet RPC errors by fjahr)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #29652 (wallet: Avoid potentially writing incorrect best block locator by ryanofsky)
    • #28710 (Remove the legacy wallet and BDB dependency by achow101)
    • #28616 (Show transactions as not fully confirmed during background validation by Sjors)
    • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)
    • #27286 (wallet: Keep track of the wallet’s own transaction outputs in memory by achow101)
    • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. DrahtBot added the label Wallet on Jun 3, 2024
  4. Thompson1985 approved
  5. ryanofsky commented at 3:34 pm on June 4, 2024: contributor

    I’m not sure, but it seems like a potentially good thing to me that last_block_processed and BESTBLOCK are two distinct concepts.

    The last block processed is the block that CWallet in-memory state has been synced to (particularly CWalletTx::m_state which includes mempool / abandoned information not serialized to disk).

    The BESTBLOCK record is the last block whose data has been flushed to disk, that the wallet should begin syncing from next time it is reloaded.

    It seems like the first commit ae3b8280a7dbb6cc87814ed97dbea5a122cf8215 could lead to a performance regression if it is writing the flushing the BESTBLOCK record on every single blockConnected event from the node. For example during reindexing it could write and sync the database each time a block is connected, even if the block is before the wallets birthday, or doesn’t have any relevant transactions?

    I think it probably makes sense to write BESTBLOCK in a smarter way, and probably write it more frequently, but it seems unnecessary to have to write it every block, and it might be bad for performance now or make optimizations harder in the future.

    I’m also curious about the idea of saving height with the best block record. I could imagine this being useful, since IIRC parts of wallet loading are complicated by not knowing heights of transactions before the chain is attached, but it doesn’t really seem like the height is used for much yet. Was this intended to be used by something else later?

  6. achow101 commented at 3:48 pm on June 4, 2024: member

    The last block processed is the block that CWallet in-memory state has been synced to (particularly CWalletTx::m_state which includes mempool / abandoned information not serialized to disk).

    But none of that state is relevant to the last block processed. Any state related to blocks (confirmed or conflicted) is written to disk.

    The BESTBLOCK record is the last block whose data has been flushed to disk, that the wallet should begin syncing from next time it is reloaded.

    The point is that this discrepancy can result in skipping blocks. It doesn’t make sense to me that we wouldn’t store the block the wallet’s state has been synced to, and it doesn’t make sense that we should rely on a field that means something else to determine what point to start rescanning from on the next load.

    If BESTBLOCK doesn’t match the chainstate, that’s fine because it’s a locator and we will just find the fork and sync from there. I don’t think it’s necessary to record which block we think the chainstate is synced to.

  7. ryanofsky commented at 3:56 pm on June 4, 2024: contributor

    It doesn’t make sense to me that we wouldn’t store the block the wallet’s state has been synced to

    You may be right this is the better approach. But I think the previous approach does make some sense, too. You might not want to write to the wallet database each time a block is connected if the block doesn’t contain any relevant transactions, especially during reindexing.

  8. achow101 commented at 9:03 pm on June 4, 2024: member

    You might not want to write to the wallet database each time a block is connected if the block doesn’t contain any relevant transactions

    I think you would since not writing it would result in possibly rescanning that block at the next loading which takes a little bit of time, regardless of whether any relevant transactions are in the block.

    especially during reindexing.

    Perhaps an easy solution to this is to just write the best block every 1000 blocks (or some other interval) when we are in IBD?

    I’m also curious about the idea of saving height with the best block record. I could imagine this being useful, since IIRC parts of wallet loading are complicated by not knowing heights of transactions before the chain is attached, but it doesn’t really seem like the height is used for much yet. Was this intended to be used by something else later?

    This was mainly done to avoid looking up the height every time we load since that was being problematic and causing a tests to fail. But it definitely could be more useful in the future.

  9. ryanofsky commented at 3:16 pm on June 5, 2024: contributor

    The idea of saving heights is interesting to me because wallet code assumes it know block heights many places, but it doesn’t actually store heights anywhere. So for example, if the wallet stored a mapping of block hashes to heights (or other block information) it might be useful for allowing the wallet to work in an offline mode or letting the wallet CLI tool have more capabilities. This is all pretty abstract though, so I’m not suggesting something specific.

    Perhaps an easy solution to this is to just write the best block every 1000 blocks (or some other interval) when we are in IBD?

    Yes but I think that just adds back the complexity you were trying to get rid of, in a form that seems worse than the status quo. If you implement that, the wallet will be back to tracking the last block processed separately from the best block written to the database. And now, instead of the node determining when data should be flushed to disk and having all wallets flush that data simultaneously, each wallet will have internal logic deciding when to flush. This would be more complicated, and could be worse for power consumption and performance if there are multiple wallets and flushes are happening more frequently at different times.

    I think overall this PR is doing some good things, but the goals seem either not clear, or not clearly good, so I wonder if maybe you could take these changes and make more targeted PRs for the things you want to achieve?

    Or, if this PR is definitely the direction you want to go, I’m happy to review it. I don’t like some things it is doing, but overall I think it is a reasonable change.

  10. DrahtBot added the label Needs rebase on Jul 22, 2024
  11. achow101 force-pushed on Jul 22, 2024
  12. DrahtBot removed the label Needs rebase on Jul 22, 2024
  13. DrahtBot added the label Needs rebase on Aug 12, 2024
  14. in test/functional/wallet_assumeutxo.py:99 in ebf2e2863c outdated
    84@@ -84,8 +85,6 @@ def run_test(self):
    85             assert_equal(n.getblockchaininfo()[
    86                          "headers"], SNAPSHOT_BASE_HEIGHT)
    87 
    88-        w.backupwallet("backup_w.dat")
    


    fjahr commented at 11:18 am on August 20, 2024:
    I’m thinking that it might make sense to keep this backup where it is and add second backup created at 199. Then this could test that both cases work as expected, i.e. 199 errors below and 299 doesn’t. 299 is an interesting edge case in general (snapshot height == backup height).
  15. fjahr commented at 11:28 am on August 20, 2024: contributor

    Concept ACK

    I can confirm that best block locator and last_processed_block being different is confusing, see also #30455 (review)

    Currently, this needs a rebase and I’m curious if @achow101 plans to make further changes based on @ryanofsky ’s comments.

  16. ryanofsky commented at 2:28 pm on August 20, 2024: contributor

    I can confirm that best block locator and last_processed_block being different is confusing

    I wonder if something else could be done to resolve this confusion other than writing to every wallet file every time a new block is connected, even if the block doesn’t contain any relevant transactions. To me, “last block processed” and “last block flushed” seem like different concepts, and we could force them to be the same but only if we:

    • Give up flexibility of not needing to flush each wallet each time a block is connected (which seems inefficient during sync)
    • Give up ability to do coordinated flushes across the chainstate database, index databases and wallet databases time (which seems like it could waste resources and hurt system performance)
  17. fjahr commented at 2:35 pm on August 20, 2024: contributor

    I wonder if something else could be done to resolve this confusion

    Ok, I would say it’s a problem when users see behavior that is inconsistent because of this difference. If the users don’t notice it I guess these don’t have to be the same at all times. There could be a more “pull-based” approach where they are aligned when the user interacts with the wallet, similar to what I tried to do for the backup case in #30678. But I am not clear yet on which approach is the better one all factors considered.

  18. ryanofsky commented at 2:48 pm on August 20, 2024: contributor
    It would be great if we could open an issue describing the user behavior that’s confusing. I read the comment you linked to but I didn’t really understand the context of what was going on with those backups. If there can be a github issue that simply describes steps to reproduce, expected behavior, and actual behavior, we can probably figure out if expected behavior is reasonable, and implement a fix if so.
  19. fjahr commented at 7:43 pm on August 20, 2024: contributor

    It would be great if we could open an issue describing the user behavior that’s confusing.

    I have created an issue here: #30686

  20. achow101 force-pushed on Aug 29, 2024
  21. Thompson1985 approved
  22. Thompson1985 commented at 8:08 pm on August 29, 2024: none
    Thank you 👍
  23. DrahtBot removed the label Needs rebase on Aug 29, 2024
  24. DrahtBot added the label Needs rebase on Sep 23, 2024
  25. achow101 force-pushed on Oct 4, 2024
  26. achow101 force-pushed on Oct 4, 2024
  27. DrahtBot removed the label Needs rebase on Oct 4, 2024
  28. DrahtBot commented at 9:52 pm on October 4, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/31102970538

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  29. DrahtBot added the label CI failed on Oct 4, 2024
  30. maflcko commented at 3:35 pm on October 16, 2024: member
    Maybe mark as draft while CI is red?
  31. DrahtBot added the label Needs rebase on Oct 24, 2024
  32. achow101 force-pushed on Oct 24, 2024
  33. DrahtBot removed the label Needs rebase on Oct 24, 2024
  34. DrahtBot added the label Needs rebase on Oct 25, 2024
  35. achow101 force-pushed on Oct 25, 2024
  36. DrahtBot removed the label Needs rebase on Oct 25, 2024
  37. DrahtBot removed the label CI failed on Oct 25, 2024
  38. DrahtBot added the label Needs rebase on Nov 15, 2024
  39. wallet: Update best block record after block dis/connect 2490986156
  40. scripted-diff: Rename LastBlock to BestBlock
    -BEGIN VERIFY SCRIPT-
    sed -i "s/SetLastBlockProcessed/SetBestBlock/" $(git grep -l "SetLastBlockProcessed")
    sed -i "s/GetLastBlock/GetBestBlock/g" $(git grep -l "GetLastBlock")
    -END VERIFY SCRIPT-
    39bfbc1628
  41. wallet: Combine last block processed hash and height into a struct
    m_last_block_processed and m_last_block_processed_height are combined
    into a new struct BestBlock.
    e17cc1f3b0
  42. wallet: Replace chainStateFlushed in loading with WriteBestBlock
    The only reason to call chainStateFlushed during wallet loading is to
    ensure that the best block is written. Do these writes explicitly to
    prepare for removing chainStateFlushed.
    3c951f7774
  43. wallet: Remove WriteBestBlock from chainStateFlushed bab0d89a2a
  44. wallet: Sync m_best_block with BESTBLOCK_NOMERKLE
    The BestBlock struct now contains the block locator, and is also
    serialized with the block height. For backwards compatibility, the
    height is optional. The best block hash is retrieved from the block
    locator when deserializing.
    
    Since the best block record has to store the height, additional
    changes to AttachChain are made to faciliate auto upgrade as well as
    setting the in-memory best block.
    207fd1b93f
  45. wallet: Load best block record in LoadWallet
    Instead of setting the best block info later during AttachChain, read it
    during LoadWallet so it is set (if it exists) before we get there.
    64facd58c4
  46. wallet: Use best block locator in migrate instead of re-reading it
    Instead of calling ReadBestBlock to get the best block locator, use the
    m_best_block.m_locator that we already have in CWallet.
    1f209f9d52
  47. wallet: use SetBestBlock when saving progress during rescan
    Instead of writing the best block record directly during a rescan, use
    SetBestBlock. This is safe because progress is only saved in a
    rescan when doing a rescan on loading. cs_wallet is held the entire time
    so new blocks or reorgs will not cause the best block record to be
    changed unexpectedly.
    151c733c16
  48. wallet: Remove chainStateFlushed
    chainStateFlushed is no longer needed since the best block is updated
    after a block is scanned. Since the chainstate being flushed does not
    necessarily coincide with the wallet having processed said block, it
    does not entirely make sense for the wallet to be recording that block
    as its best block, and this can cause race conditions where some blocks
    are not processed. Thus, remove this notification.
    d4c7013bec
  49. achow101 force-pushed on Nov 19, 2024
  50. DrahtBot removed the label Needs rebase on Nov 19, 2024

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-12-21 15:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me