Merge bitcoin/bitcoin#32895: wallet: Prepare for future upgrades by recording versions of last client to open and decrypt
What changed, and why it matters
This change is a wallet infrastructure improvement, not a fix for an active security bug. It records which Bitcoin Core version and feature set last opened or decrypted a wallet so that future releases can detect 'upgrade-downgrade-upgrade' cycles and re-run automatic wallet upgrades safely. There is no direct exploit here, but any bug in this bookkeeping could in theory cause a wallet to skip needed upgrades or behave unexpectedly after being loaded in older software.
Review the new version/feature bookkeeping for correctness and edge cases (e.g., partial writes, migration failures, encrypted wallets that are never unlocked). Monitor follow-up PRs that will rely on these records to trigger automatic upgrades, since the actual security relevance depends on those future changes.
Security signals we found
New wallet database records for tracking last client version/features
Decoupling of wallet version metadata from node CLIENT_VERSION
Erase of stale decryption-features record on downgrade detection
Feature flags introduced to drive future automatic wallet upgrades
No patch of an existing vulnerability; preparatory change for future upgrades
Evidence from the diff
The commit decouples the wallet’s ‘version’ record from the node CLIENT_VERSION, introduces a new WalletClientVersion enum (starting at 1<<19 + 1), and adds two new wallet database records: LAST_OPENED_FEATURES and LAST_DECRYPTED_FEATURES. These records store a WALLET_CLIENT_FEATURES bitfield so future clients can detect when a wallet was last touched by an older client and decide whether automatic upgrades are required. The records are written on wallet creation, migration, unlock, and successful load. If a wallet was last opened by a client that predates these records, the stale LAST_DECRYPTED_FEATURES record is erased to prevent downgrade confusion.
Changed components
src/wallet/wallet.cppsrc/wallet/wallet.hsrc/wallet/walletdb.cppsrc/wallet/walletdb.hsrc/wallet/walletutil.htest/functional/wallet_createwallet.pyInspect captured patch +136 / −18
### src/wallet/wallet.cpp
@@ -620,6 +620,17 @@ util::Expected<void, WalletError> CWallet::Unlock(const SecureString& strWalletP
if (Unlock(plain_master_key)) {
// Now that we've unlocked, upgrade the descriptor cache
UpgradeDescriptorCache();
+
+ if (!m_last_decrypted_features || *m_last_decrypted_features != WALLET_CLIENT_FEATURES) {
+ // Write the current wallet client features to LAST_DECRYPTED_FEATURES.
+ // This must be done after all automatic upgrades so that those upgrades can be
+ // performed in an upgrade-downgrade-upgrade scenario.
+ WalletBatch batch(GetDatabase());
+ if (batch.WriteLastDecryptedFeatures()) {
+ SetLastDecryptedFeatures(WALLET_CLIENT_FEATURES);
+ }
+ }
+
return {};
}
}
@@ -2937,9 +2948,16 @@ std::shared_ptr<CWallet> CWallet::CreateNew(WalletContext& context, const std::s
}
// Initialize version key.
- if(!WalletBatch(walletInstance->GetDatabase()).WriteVersion(CLIENT_VERSION)) {
- error = strprintf(_("Error creating %s: Could not write version metadata."), walletFile);
- return nullptr;
+ {
+ WalletBatch batch(walletInstance->GetDatabase());
+ if(!batch.WriteLastOpenedVersion()) {
+ error = strprintf(_("Error creating %s: Could not write version metadata."), walletFile);
+ return nullptr;
+ }
+ if(!batch.WriteLastOpenedFeatures()) {
+ error = strprintf(_("Error creating %s: Could not write features metadata."), walletFile);
+ return nullptr;
+ }
}
{
LOCK(walletInstance->cs_wallet);
@@ -3885,6 +3903,19 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
}
}
+ // Set the last opened version and features
+ if (!local_wallet_batch.WriteLastOpenedVersion()) {
+ return util::Error{_("Error: Unable to write last opened version")};
+ }
+ if (!local_wallet_batch.WriteLastOpenedFeatures()) {
+ return util::Error{_("Error: Unable to write last opened features")};
+ }
+ if (HasEncryptionKeys()) {
+ if (!local_wallet_batch.WriteLastDecryptedFeatures()) {
+ return util::Error{_("Error: Unable to write last decrypted features")};
+ }
+ }
+
// Get best block locator so that we can copy it to the watchonly and solvables
// Note: The best block locator was introduced in #152 so ancient wallets do not have it
CBlockLocator best_block_locator;
@@ -4527,4 +4558,9 @@ void CWallet::DisconnectChainNotifications()
}
}
+void CWallet::SetLastDecryptedFeatures(uint64_t features)
+{
+ AssertLockHeld(cs_wallet);
+ m_last_decrypted_features = features;
+}
} // namespace wallet
### src/wallet/wallet.h
@@ -428,6 +428,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
//! Set of both spent and unspent transaction outputs owned by this wallet
std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher> m_txos GUARDED_BY(cs_wallet);
+ //! Features of the last client to decrypt this wallet
+ std::optional<uint64_t> m_last_decrypted_features GUARDED_BY(cs_wallet);
+
/**
* Catch wallet up to current chain, scanning new blocks, updating the best
* block locator and m_last_block_processed, and registering for
@@ -1083,6 +1086,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
//! Disconnect chain notifications and wait for all notifications to be processed
void DisconnectChainNotifications();
+
+ //! Set the features of the last client to decrypt this wallet
+ void SetLastDecryptedFeatures(uint64_t features) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
};
/**
### src/wallet/walletdb.cpp
@@ -42,6 +42,8 @@ const std::string FLAGS{"flags"};
const std::string HDCHAIN{"hdchain"};
const std::string KEYMETA{"keymeta"};
const std::string KEY{"key"};
+const std::string LAST_DECRYPTED_FEATURES{"lastdecryptedfeatures"};
+const std::string LAST_OPENED_FEATURES{"lastopenedfeatures"};
const std::string LOCKED_UTXO{"lockedutxo"};
const std::string MASTER_KEY{"mkey"};
const std::string MINVERSION{"minversion"};
@@ -781,7 +783,7 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
desc.emplace(WalletDescriptor::FromStream(deserialize, value));
} catch (const std::ios_base::failure& e) {
strErr = strprintf("Error: Unrecognized descriptor found in wallet %s. ", pwallet->GetName());
- strErr += (last_client > CLIENT_VERSION) ? "The wallet might have been created on a newer version. " :
+ strErr += (last_client > VERSION_LATEST) ? "The wallet might have been created on a newer version. " :
"The database might be corrupted or the software version is not compatible with one of your wallet descriptors. ";
strErr += "Please try running the latest software version";
// Also include error details
@@ -1152,10 +1154,23 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
LOCK(pwallet->cs_wallet);
// Last client version to open this wallet
- int last_client = CLIENT_VERSION;
+ int last_client = VERSION_LATEST;
bool has_last_client = m_batch->Read(DBKeys::VERSION, last_client);
if (has_last_client) pwallet->WalletLogPrintf("Last client version = %d\n", last_client);
+ std::optional<uint64_t> last_client_features;
+ if (last_client >= VERSION_LAST_CLIENT_FEATURES) {
+ // Features of last client to open this wallet
+ if (uint64_t features; m_batch->Read(DBKeys::LAST_OPENED_FEATURES, features)) {
+ last_client_features = features;
+ }
+
+ // Features of last client to decrypt this wallet
+ if (uint64_t last_decrypted; m_batch->Read(DBKeys::LAST_DECRYPTED_FEATURES, last_decrypted)) {
+ pwallet->SetLastDecryptedFeatures(last_decrypted);
+ }
+ }
+
try {
// Load wallet flags, so they are known when processing other records.
// The FLAGS key is absent during wallet creation.
@@ -1206,9 +1221,6 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
if (result != DBErrors::LOAD_OK)
return result;
- if (!has_last_client || last_client != CLIENT_VERSION) // Update
- this->WriteVersion(CLIENT_VERSION);
-
if (any_unordered)
result = pwallet->ReorderTransactions();
@@ -1235,6 +1247,32 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
pwallet->mapMasterKeys.clear();
}
+ // Discard stale decryption features before updating the version, so that a locked
+ // reload cannot restore the features recorded before a downgrade.
+ if (last_client < VERSION_LAST_CLIENT_FEATURES && m_batch->Exists(DBKeys::LAST_DECRYPTED_FEATURES)) {
+ if (!EraseIC(DBKeys::LAST_DECRYPTED_FEATURES)) {
+ pwallet->WalletLogPrintf("Error: Unable to erase last decrypted client features.\n");
+ return DBErrors::LOAD_FAIL;
+ }
+ }
+
+ // Record the current client version as the last version to successfully open this wallet file
+ // This must always be done after all automatic upgrades so that those upgrades can be performed
+ // in an upgrade-downgrade-upgrade scenario.
+ if (!has_last_client || last_client != VERSION_LATEST) {
+ if (!WriteLastOpenedVersion()) {
+ pwallet->WalletLogPrintf("Error: Unable to write the last opened version. Wallet corrupt.\n");
+ return DBErrors::CORRUPT;
+ }
+ }
+ // Record the current client features as the features of the last client to successfully open this wallet file.
+ if (!last_client_features || *last_client_features != WALLET_CLIENT_FEATURES) {
+ if (!WriteLastOpenedFeatures()) {
+ pwallet->WalletLogPrintf("Error: Unable to write the last opened features. Wallet corrupt.\n");
+ return DBErrors::CORRUPT;
+ }
+ }
+
return result;
}
@@ -1295,6 +1333,21 @@ bool WalletBatch::WriteWalletFlags(const uint64_t flags)
return WriteIC(DBKeys::FLAGS, flags);
}
+bool WalletBatch::WriteLastOpenedVersion()
+{
+ return WriteIC(DBKeys::VERSION, VERSION_LATEST);
+}
+
+bool WalletBatch::WriteLastOpenedFeatures()
+{
+ return WriteIC(DBKeys::LAST_OPENED_FEATURES, WALLET_CLIENT_FEATURES);
+}
+
+bool WalletBatch::WriteLastDecryptedFeatures()
+{
+ return WriteIC(DBKeys::LAST_DECRYPTED_FEATURES, WALLET_CLIENT_FEATURES);
+}
+
bool WalletBatch::EraseRecords(const std::unordered_set<std::string>& types)
{
return std::all_of(types.begin(), types.end(), [&](const std::string& type) {
### src/wallet/walletdb.h
@@ -70,6 +70,8 @@ extern const std::string FLAGS;
extern const std::string HDCHAIN;
extern const std::string KEY;
extern const std::string KEYMETA;
+extern const std::string LAST_DECRYPTED_FEATURES;
+extern const std::string LAST_OPENED_FEATURES;
extern const std::string LOCKED_UTXO;
extern const std::string MASTER_KEY;
extern const std::string MINVERSION;
@@ -273,19 +275,18 @@ class WalletBatch
DBErrors LoadWallet(CWallet* pwallet);
- /**
- * Write the given `client_version` to m_batch, indicating the last version
- * of client software to load this wallet.
- *
- * @param[in] client_version `CLIENT_VERSION` outside of test code.
- * @return A bool indicating whether or not the write succeeded.
- */
- bool WriteVersion(int client_version) { return m_batch->Write(DBKeys::VERSION, client_version); }
+ //! Write the current client version in the VERSION record
+ [[nodiscard]] bool WriteLastOpenedVersion();
+ //! Write the current client features in the LAST_OPENED_FEATURES record
+ [[nodiscard]] bool WriteLastOpenedFeatures();
+ //! Write the current wallet client features to the LAST_DECRYPTED_FEATURES record
+ [[nodiscard]] bool WriteLastDecryptedFeatures();
//! Delete records of the given types
bool EraseRecords(const std::unordered_set<std::string>& types);
bool WriteWalletFlags(uint64_t flags);
+
//! Begin a new transaction
bool TxnBegin();
//! Commit current transaction
### src/wallet/walletutil.h
@@ -56,6 +56,28 @@ enum WalletFlags : uint64_t {
WALLET_FLAG_EXTERNAL_SIGNER = (1ULL << 35),
};
+// Version numbers for the wallet client that opens a wallet
+// These numbers will be written as the last client version in the "version" record and can be used to detect
+// when an upgrade-downgrade-upgrade was performed. However, we should prefer to use LastClientFeatures rather
+// than new version numbers.
+// New version numbers must be greater than 329900 which is guaranteed by setting bit 19
+enum WalletClientVersion : int32_t {
+ MIN_VERSION = (1L << 19),
+
+ // The wallet client supports the records for LastClientFeatures
+ VERSION_LAST_CLIENT_FEATURES = MIN_VERSION + 1,
+
+ VERSION_LATEST = VERSION_LAST_CLIENT_FEATURES
+};
+
+enum LastClientFeatures : uint64_t {
+ // Flags indicating the automatic upgrade features supported by the wallet client that last opened a wallet file
+ // New automatic upgrades must define a flag here so that upgrade-downgrade-upgrade can be detected to determine whether
+ // an automatic upgrade should be performed.
+
+ WALLET_CLIENT_FEATURES = 0
+};
+
//! Get the path of the wallet directory.
fs::path GetWalletDir();
### test/functional/wallet_createwallet.py
@@ -20,6 +20,7 @@
EMPTY_PASSPHRASE_MSG = "Empty string given as passphrase, wallet will not be encrypted."
+WALLET_CLIENT_VERSION = 0x80001
class CreateWalletTest(BitcoinTestFramework):
@@ -192,8 +193,7 @@ def run_test(self):
self.log.info("Check that the version number is being logged correctly")
# Craft the expected version message.
- client_version = node.getnetworkinfo()["version"]
- version_message = f"Last client version = {client_version}"
+ version_message = f"Last client version = {WALLET_CLIENT_VERSION}"
# Should not be logged when creating.
with node.assert_debug_log(expected_msgs=[], unexpected_msgs=[version_message]):Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.