Skip to content

wallet: Prepare for future upgrades by recording versions of last client to open and decrypt #32895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

achow101
Copy link
Member

@achow101 achow101 commented Jul 7, 2025

When a wallet is automatically upgraded, we always do so in a way that allows the user to load their wallet in an older version. When the user loads their wallet into an upgraded version again, the wallet may not perform the automatic upgrade and end up with upgraded and non-upgraded material in the wallet.

If we write some information about the last client to open the wallet, we would be able to detect these upgrade-downgrade-upgrade situations and perform another upgrade if so. However, in order for this to work, we need to have implemented writing such information into versions prior to the ones which introduce new automatic upgrades.

We are already writing the CLIENT_VERSION into all wallets in a "version" record, although this record is not updated in the same way across all previous versions. Since v24.0, we are always updating the record when the client version differs, but prior to that, the record would only be updated when the client version is newer. Even so, we can use this record to store the last client version.

The first commit decouples the written client version from the node version. This aids in development of new features. The version is entirely decoupled from previous versions by requiring all versions to have bit 30 set, and the initial version being 0 | (1 << 30). The versions just need to be at least 299900, and forcing bit 30 to be set was an easy way to achieve this.

However, using version numbers is not as flexible as feature flags, so the second commit introduces the concept of Wallet Client Features. These flags are distinct from the existing Wallet Feature Flags since they are conceptually different. These are written in a separate record, and the wallet client version is incremented to (1 << 0) | (1 << 30).

Lastly, since some upgrades may need private keys and can only occur after the wallet is decrypted, the third commit also adds a last decrypted features flags record to record the features of the last client to decrypt the wallet.

The result is that we will now be able to detect a downgrade down to v24.0 and be able to implement in parallel multiple features that require automatic upgrades.

@DrahtBot DrahtBot added the Wallet label Jul 7, 2025
@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 7, 2025

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/32895.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK ryanofsky
Stale ACK w0xlt

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:

  • #33034 (wallet: Store transactions in a separate sqlite table by achow101)
  • #32685 (wallet: Allow read-only database access for info and dump commands by PeterWrighten)
  • #32636 (Split CWallet::Create() into CreateNew and LoadExisting by davidgumberg)
  • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
  • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)

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.

LLM Linter (✨ experimental)

Possible typos and grammar issues:

  • “when a upgrade-downgrade-upgrade was performed” -> “when an upgrade-downgrade-upgrade was performed” [‘upgrade’ begins with a vowel sound, so needs “an”]
  • “LastClientFEatures” -> “LastClientFeatures” [capitalization typo in comment]

drahtbot_id_4_m

@maflcko
Copy link
Member

maflcko commented Jul 8, 2025

I wonder if this can be uncoupled from CLIENT_VERSION, because the coupling is confusing and makes testing harder:

  • It is confusing, because switching between versions that kept the wallet code the same will pretend that there was a relevant upgrade/downgrade, even though the wallet behavior is exactly identical.
  • CLIENT_VERSION on the development branch is changed every 6 months, not when a new wallet feature was added that needs to deal with un-upgraded material. I know that running from the master branch is not supported and devs will have to deal with un-upgraded material themselves, but it seems easy to fix (and cleaner) to just introduce a new variable in the wallet that is equal to the current CLIENT_VERSION and then handle it separately and explicitly from now on.

@ryanofsky
Copy link
Contributor

Concept ACK. It shouldn't hurt to add more metadata to help detect when there might be a mix of old data in a newer format and new data in an older format in the same wallet.

Conceptually, I think we could always detect these cases by just scanning the entire wallet for data that needs to be upgraded on loading. But having this extra metadata is nice because it can avoid slowing down wallet loading by avoiding unnecessary upgrade rescans.

On the approach in e94ef51, I think it is ok, but would suggest some ways it might be improved:

  • I agree with Marco's comment that it would be great to move away from CLIENT_VERSION and use something independent of the node version.
    • A downside of this suggestion is that the PR might not be able to reuse the existing VERSION field and might need to introduce 2 new fields instead of 1. I think the tradeoff could be worth it though.
  • It would be nice to avoid version numbers altogether and use feature flags instead, defining new feature flags when new data fields are introduced, and writing supported feature flags to LAST_OPENED_FEATURES & LAST_DECRYPTED_FEATURES fields on wallet loading.
    • This would not require introducing a new data type since we already have a WalletFlags enum. New internal and external features could just be new (nonmandatory) WalletFlags values.
    • IMO, flags are better than version numbers because they decouple code that implements new features from the merge and release process. They also make code more readable and maintainable because checks like if (last_opened_supported_specific_feature_x) are more obvious than if (last_opened_version > some_version_number_with_collection_of_features)
  • I assume we don't care about detecting upgrades & downgrades to versions before v24.0 which wouldn't overwrite the VERSION field on opening. But if we did, it'd be possible to detect this by writing last_opened and last_decrypted metadata to a file alongside the wallet database instead of inside the wallet database, and touching the file whenever the database is flushed. This way, if the database file modification time was greater than metadata file modification time, you could infer that the database had been loaded by an older version in the meantime and needs to be upgraded. Wouldn't recommend this approach because it is more complicated, but wanted to mention it as an alternative to introducing new database fields.

@achow101
Copy link
Member Author

achow101 commented Jul 8, 2025

I wonder if this can be uncoupled from CLIENT_VERSION, because the coupling is confusing and makes testing harder:

Definitely, however I'd like to be able to reuse the existing "version" record which mostly has the desired behavior since v24.0. The detection only works because of older versions overwriting the field with their CLIENT_VERSION. Introducing a new field now means that we cannot detect when an upgraded wallet was opened in any version older than v30.0 (assuming this is merged for the next release). The timeline would be too soon for any newly introduced features in say v31.0 or v32.0 (and I've got a few things in mind already that would utilize this). I'm already not particularly thrilled by the fact that the current implementation can only detect down to 24.0 as I expect that users doing upgrade-downgrade-upgrade with even older versions is commonplace.

Conceptually, I think we could always detect these cases by just scanning the entire wallet for data that needs to be upgraded on loading.

I don't think that is actually viable as it would largely end up performing the upgrade on all loads regardless of whether the upgrade has already been done.

@maflcko
Copy link
Member

maflcko commented Jul 8, 2025

Maybe I am missing something obvious, but my suggestion was not to introduce a new field/record. It was to re-use the existing field (and value) and manually handle the value going forward only when needed.

That is, old wallets will (obviously) continue to write their CLIENT_VERSION to the version record. Also, any new wallet will continue to write a value of the current CLIENT_VERSION to it, just like on the current development branch (this is unchanged as well). The only change is that for the next release, the wallet will continue to write the same value as today, unless a feature was added that requires bumping the version, in which case the version is bumped in the pull request introducing the feature.

@achow101
Copy link
Member Author

achow101 commented Jul 8, 2025

Maybe I am missing something obvious, but my suggestion was not to introduce a new field/record. It was to re-use the existing field (and value) and manually handle the value going forward only when needed.

Sorry, I was responding to @ryanofsky's comment at the same time since they were very similar.

I think that may be workable.

@achow101 achow101 force-pushed the wallet-lastclient branch from e94ef51 to f747394 Compare July 8, 2025 20:39
@achow101
Copy link
Member Author

achow101 commented Jul 8, 2025

I've updated the PR to do a combination of @maflcko's and @ryanofsky's suggestions.

The new approach decouples version from CLIENT_VERSION. It also writes out a set of client specific feature flags into two new records: lastopenedfeatures and lastdecryptedfeatures. However, these feature flags are separate from the existing wallet feature flags.

I believe this preserves the behavior of detecting downgrades down to v24.0, while also giving us the flexibility of a new field and feature flags.

Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK b0470f5

nit: Commit 3662960 appears to have CI failures.

@DrahtBot DrahtBot requested a review from ryanofsky July 23, 2025 21:27
@achow101
Copy link
Member Author

nit: Commit 3662960 appears to have CI failures.

I believe that's only because the tasks were canceled by a new push.

Instead of tying the last client record with the node version, introduce
a separate wallet client version that will be increased as necessary
when new automatic upgrades are introduced.
…ecord

LastClientFeatures are feature flags indicating what automatic upgrade
features supported by the last client that opened the wallet.

The LAST_OPENED_FEATURES record stores these flags and must be
set to match the exact features supported by the client that opens a
wallet.
Set these flags to avoid the automatic upgrade after migrating.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants