refactor(history): make TransactionHistoryCache safe for a writer thread
The tx-history cache was only ever used from the UI thread and had no locking. To let snapshots be written from a background thread: - every SQL statement runs under one database mutex and the key fields under a second one, taken after it and never held across I/O or the key derivation; - a write is a self-contained, move-only SnapshotJob holding the rows and a private copy of the key, which is wiped after the write or when a job never runs; the JSON build and encryption hold no lock; - a job with an all-zero key is refused instead of overwriting a good snapshot with one nobody can read; - the prune after a write also keeps the wallet unlocked now, so a late job for the previous wallet can't delete the salt of the one just switched to; - keyId() gives a non-reversible tag of the installed key, so a passphrase change can be told from a re-unlock with the same one. replace() stays a synchronous wrapper with the same result. A failed INSERT is now logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
@@ -110,8 +111,25 @@ std::string associatedDataForWallet(const std::string& walletHash)
|
||||
return std::string("obsidian-dragon-tx-history-v1:") + walletHash;
|
||||
}
|
||||
|
||||
// Non-reversible 64-bit tag of a derived key (BLAKE2b keyed by the key, over a fixed label). Only used to
|
||||
// notice that the key changed; nothing about the key can be recovered from it, and it never leaves memory.
|
||||
std::uint64_t computeKeyId(const std::array<unsigned char, 32>& key)
|
||||
{
|
||||
static const unsigned char kLabel[] = "obsidian-dragon-tx-history-keyid";
|
||||
unsigned char out[crypto_generichash_BYTES_MIN]; // 16, libsodium's minimum; the first 8 are used
|
||||
crypto_generichash(out, sizeof(out), kLabel, sizeof(kLabel) - 1, key.data(), key.size());
|
||||
std::uint64_t id = 0;
|
||||
std::memcpy(&id, out, sizeof(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TransactionHistoryCache::SnapshotJob::~SnapshotJob()
|
||||
{
|
||||
sodium_memzero(key.data(), key.size());
|
||||
}
|
||||
|
||||
TransactionHistoryCache::TransactionHistoryCache()
|
||||
: TransactionHistoryCache(defaultDatabasePath())
|
||||
{
|
||||
@@ -170,6 +188,7 @@ std::string TransactionHistoryCache::walletIdentityHash(const std::string& walle
|
||||
|
||||
bool TransactionHistoryCache::ensureOpen()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (db_) return true;
|
||||
|
||||
try {
|
||||
@@ -212,29 +231,70 @@ bool TransactionHistoryCache::ensureOpen()
|
||||
bool TransactionHistoryCache::unlockWithPassphrase(const std::string& walletIdentity,
|
||||
const std::string& passphrase)
|
||||
{
|
||||
if (walletIdentity.empty() || passphrase.empty() || !ensureOpen()) return false;
|
||||
if (walletIdentity.empty() || passphrase.empty()) return false;
|
||||
|
||||
// Held across the KDF on purpose: "this wallet's salt row exists" and "this wallet is the unlocked one"
|
||||
// must be atomic with respect to a writer job's prune (writeSnapshot keeps the unlocked wallet), or a job
|
||||
// for the previous wallet could delete the salt this key is being derived from. The UI thread only waits
|
||||
// here for a write already in flight, never for the queue.
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!ensureOpen()) return false;
|
||||
|
||||
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||
std::vector<unsigned char> salt = getOrCreateSalt(walletHash);
|
||||
if (salt.empty()) return false;
|
||||
|
||||
if (!deriveKey(passphrase, salt)) return false;
|
||||
unlocked_wallet_hash_ = std::move(walletHash);
|
||||
key_ready_ = true;
|
||||
std::array<unsigned char, kKeyBytes> derived{};
|
||||
if (!deriveKey(passphrase, salt, derived)) return false;
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
key_ = derived;
|
||||
key_id_ = computeKeyId(derived);
|
||||
unlocked_wallet_hash_ = std::move(walletHash);
|
||||
key_ready_ = true;
|
||||
}
|
||||
sodium_memzero(derived.data(), derived.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransactionHistoryCache::lockKey()
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
if (key_ready_) sodium_memzero(key_.data(), key_.size());
|
||||
key_ready_ = false;
|
||||
unlocked_wallet_hash_.clear();
|
||||
// key_id_ is kept on purpose: re-unlocking with the same passphrase yields the same id.
|
||||
}
|
||||
|
||||
bool TransactionHistoryCache::hasKey() const
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
return key_ready_;
|
||||
}
|
||||
|
||||
bool TransactionHistoryCache::isUnlockedFor(const std::string& walletIdentity) const
|
||||
{
|
||||
return key_ready_ && !walletIdentity.empty() &&
|
||||
unlocked_wallet_hash_ == walletIdentityHash(walletIdentity);
|
||||
if (walletIdentity.empty()) return false;
|
||||
const std::string hash = walletIdentityHash(walletIdentity); // outside the lock
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
return key_ready_ && unlocked_wallet_hash_ == hash;
|
||||
}
|
||||
|
||||
bool TransactionHistoryCache::captureKey(const std::string& walletIdentity,
|
||||
std::array<unsigned char, 32>& keyOut) const
|
||||
{
|
||||
if (walletIdentity.empty()) return false;
|
||||
const std::string hash = walletIdentityHash(walletIdentity);
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
if (!key_ready_ || unlocked_wallet_hash_ != hash) return false;
|
||||
keyOut = key_;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t TransactionHistoryCache::keyId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
return key_id_;
|
||||
}
|
||||
|
||||
TransactionHistoryCache::LoadResult TransactionHistoryCache::load(
|
||||
@@ -243,6 +303,7 @@ TransactionHistoryCache::LoadResult TransactionHistoryCache::load(
|
||||
const std::string& currentTipHash)
|
||||
{
|
||||
LoadResult result;
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_); // serialised against the writer's INSERT/prune
|
||||
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return result;
|
||||
|
||||
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||
@@ -303,21 +364,42 @@ bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
||||
std::time_t updatedAt,
|
||||
const std::unordered_map<std::string, int>& shieldedScanHeights)
|
||||
{
|
||||
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return false;
|
||||
// The synchronous form (tests, tools): the same write the background writer does.
|
||||
SnapshotJob job;
|
||||
if (!captureKey(walletIdentity, job.key)) return false;
|
||||
job.walletIdentity = walletIdentity;
|
||||
job.tipHeight = tipHeight;
|
||||
job.tipHash = tipHash;
|
||||
job.transactions = transactions;
|
||||
job.updatedAt = updatedAt;
|
||||
job.shieldedScanHeights = shieldedScanHeights;
|
||||
return writeSnapshot(std::move(job));
|
||||
}
|
||||
|
||||
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||
bool TransactionHistoryCache::writeSnapshot(SnapshotJob&& job)
|
||||
{
|
||||
struct WipeKey {
|
||||
std::array<unsigned char, 32>& key;
|
||||
~WipeKey() { sodium_memzero(key.data(), key.size()); }
|
||||
} wipe{job.key};
|
||||
|
||||
// A job without a key would replace the good snapshot with one nobody can read.
|
||||
if (job.walletIdentity.empty() || job.key == std::array<unsigned char, 32>{}) return false;
|
||||
const std::string walletHash = walletIdentityHash(job.walletIdentity);
|
||||
|
||||
// ---- the expensive part (JSON DOM, dump, AEAD), with no lock held ----
|
||||
json payload;
|
||||
payload["schema_version"] = kSchemaVersion;
|
||||
payload["wallet_hash"] = walletHash;
|
||||
payload["tip_height"] = tipHeight;
|
||||
payload["tip_hash"] = tipHash;
|
||||
payload["updated_at"] = static_cast<std::int64_t>(updatedAt);
|
||||
payload["tip_height"] = job.tipHeight;
|
||||
payload["tip_hash"] = job.tipHash;
|
||||
payload["updated_at"] = static_cast<std::int64_t>(job.updatedAt);
|
||||
payload["transactions"] = json::array();
|
||||
for (const auto& transaction : transactions) {
|
||||
for (const auto& transaction : job.transactions) {
|
||||
payload["transactions"].push_back(transactionToJson(transaction));
|
||||
}
|
||||
payload["shielded_scan_heights"] = json::object();
|
||||
for (const auto& [address, height] : shieldedScanHeights) {
|
||||
for (const auto& [address, height] : job.shieldedScanHeights) {
|
||||
if (!address.empty() && height >= 0) {
|
||||
payload["shielded_scan_heights"][address] = height;
|
||||
}
|
||||
@@ -326,10 +408,14 @@ bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
||||
std::string plainText = payload.dump();
|
||||
std::vector<unsigned char> nonce;
|
||||
std::vector<unsigned char> cipherText;
|
||||
bool encrypted = encryptPayload(walletHash, plainText, nonce, cipherText);
|
||||
const bool encrypted = encryptPayload(job.key, walletHash, plainText, nonce, cipherText);
|
||||
sodium_memzero(plainText.data(), plainText.size());
|
||||
if (!encrypted) return false;
|
||||
|
||||
// ---- one autocommit INSERT OR REPLACE (WAL: a crash leaves the previous snapshot), then the prune ----
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!ensureOpen()) return false;
|
||||
|
||||
Statement statement(db_,
|
||||
"INSERT OR REPLACE INTO transaction_history_snapshots "
|
||||
"(wallet_hash, schema_version, tip_height, tip_hash, updated_at, nonce, ciphertext) "
|
||||
@@ -338,14 +424,22 @@ bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
||||
|
||||
if (!bindText(statement.handle, 1, walletHash)) return false;
|
||||
sqlite3_bind_int(statement.handle, 2, kSchemaVersion);
|
||||
sqlite3_bind_int(statement.handle, 3, std::max(0, tipHeight));
|
||||
if (!bindText(statement.handle, 4, tipHash)) return false;
|
||||
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(updatedAt));
|
||||
sqlite3_bind_int(statement.handle, 3, std::max(0, job.tipHeight));
|
||||
if (!bindText(statement.handle, 4, job.tipHash)) return false;
|
||||
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(job.updatedAt));
|
||||
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
||||
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
||||
|
||||
if (sqlite3_step(statement.handle) != SQLITE_DONE) return false;
|
||||
pruneOtherWallets(walletHash); // bound DB growth — drop stale-hash snapshots/salts
|
||||
if (sqlite3_step(statement.handle) != SQLITE_DONE) {
|
||||
DEBUG_LOGF("Transaction history cache write failed: %s\n", sqlite3_errmsg(db_));
|
||||
return false;
|
||||
}
|
||||
std::string liveHash;
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
liveHash = unlocked_wallet_hash_;
|
||||
}
|
||||
pruneOtherWallets(walletHash, liveHash); // bound DB growth — drop stale-hash snapshots/salts
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -357,6 +451,7 @@ void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
||||
|
||||
int TransactionHistoryCache::snapshotCount()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!ensureOpen()) return 0;
|
||||
Statement statement(db_, "SELECT COUNT(*) FROM transaction_history_snapshots");
|
||||
if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
|
||||
@@ -418,7 +513,8 @@ std::vector<unsigned char> TransactionHistoryCache::getOrCreateSalt(const std::s
|
||||
}
|
||||
|
||||
bool TransactionHistoryCache::deriveKey(const std::string& passphrase,
|
||||
const std::vector<unsigned char>& salt)
|
||||
const std::vector<unsigned char>& salt,
|
||||
std::array<unsigned char, 32>& keyOut)
|
||||
{
|
||||
if (salt.size() != crypto_pwhash_SALTBYTES) return false;
|
||||
unsigned char derived[kKeyBytes];
|
||||
@@ -429,17 +525,17 @@ bool TransactionHistoryCache::deriveKey(const std::string& passphrase,
|
||||
crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
crypto_pwhash_ALG_ARGON2ID13);
|
||||
if (result != 0) return false;
|
||||
std::copy(derived, derived + sizeof(derived), key_.begin());
|
||||
std::copy(derived, derived + sizeof(derived), keyOut.begin());
|
||||
sodium_memzero(derived, sizeof(derived));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TransactionHistoryCache::encryptPayload(const std::string& walletHash,
|
||||
bool TransactionHistoryCache::encryptPayload(const std::array<unsigned char, 32>& key,
|
||||
const std::string& walletHash,
|
||||
const std::string& plainText,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipherText) const
|
||||
std::vector<unsigned char>& cipherText)
|
||||
{
|
||||
if (!key_ready_) return false;
|
||||
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||
randombytes_buf(nonce.data(), nonce.size());
|
||||
|
||||
@@ -450,7 +546,7 @@ bool TransactionHistoryCache::encryptPayload(const std::string& walletHash,
|
||||
cipherText.data(), &cipherLength,
|
||||
reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size(),
|
||||
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||
nullptr, nonce.data(), key_.data());
|
||||
nullptr, nonce.data(), key.data());
|
||||
if (result != 0) return false;
|
||||
cipherText.resize(static_cast<std::size_t>(cipherLength));
|
||||
return true;
|
||||
@@ -461,10 +557,16 @@ bool TransactionHistoryCache::decryptPayload(const std::string& walletHash,
|
||||
const std::vector<unsigned char>& cipherText,
|
||||
std::string& plainText) const
|
||||
{
|
||||
if (!key_ready_ || nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES ||
|
||||
if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES ||
|
||||
cipherText.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) {
|
||||
return false;
|
||||
}
|
||||
std::array<unsigned char, kKeyBytes> key{};
|
||||
{
|
||||
std::lock_guard<std::mutex> keyLock(key_mutex_);
|
||||
if (!key_ready_) return false;
|
||||
key = key_;
|
||||
}
|
||||
|
||||
std::string associatedData = associatedDataForWallet(walletHash);
|
||||
std::vector<unsigned char> plain(cipherText.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||
@@ -473,7 +575,8 @@ bool TransactionHistoryCache::decryptPayload(const std::string& walletHash,
|
||||
plain.data(), &plainLength, nullptr,
|
||||
cipherText.data(), cipherText.size(),
|
||||
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||
nonce.data(), key_.data());
|
||||
nonce.data(), key.data());
|
||||
sodium_memzero(key.data(), key.size());
|
||||
if (result != 0) return false;
|
||||
|
||||
plainText.assign(reinterpret_cast<const char*>(plain.data()), static_cast<std::size_t>(plainLength));
|
||||
@@ -506,6 +609,7 @@ bool TransactionHistoryCache::readSnapshot(const std::string& walletHash,
|
||||
|
||||
void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!ensureOpen()) return;
|
||||
Statement statement(db_, "DELETE FROM transaction_history_snapshots WHERE wallet_hash = ?");
|
||||
if (!statement.handle) return;
|
||||
@@ -513,22 +617,28 @@ void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
||||
sqlite3_step(statement.handle);
|
||||
}
|
||||
|
||||
void TransactionHistoryCache::pruneOtherWallets(const std::string& keepWalletHash)
|
||||
void TransactionHistoryCache::pruneOtherWallets(const std::string& keepWalletHash,
|
||||
const std::string& alsoKeepWalletHash)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!ensureOpen() || keepWalletHash.empty()) return;
|
||||
const std::string& alsoKeep = alsoKeepWalletHash.empty() ? keepWalletHash : alsoKeepWalletHash;
|
||||
// Table names are hardcoded literals (no injection surface). Prune both the snapshot
|
||||
// blobs and the now-orphaned salt rows so a stale salt can't outlive its ciphertext.
|
||||
for (const char* table : {"transaction_history_snapshots", "transaction_history_keys"}) {
|
||||
const std::string sql = std::string("DELETE FROM ") + table + " WHERE wallet_hash <> ?";
|
||||
const std::string sql = std::string("DELETE FROM ") + table +
|
||||
" WHERE wallet_hash <> ? AND wallet_hash <> ?";
|
||||
Statement statement(db_, sql.c_str());
|
||||
if (!statement.handle) continue;
|
||||
if (!bindText(statement.handle, 1, keepWalletHash)) continue;
|
||||
if (!bindText(statement.handle, 2, alsoKeep)) continue;
|
||||
sqlite3_step(statement.handle);
|
||||
}
|
||||
}
|
||||
|
||||
void TransactionHistoryCache::close()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> dbLock(db_mutex_);
|
||||
if (!db_) return;
|
||||
sqlite3_close(db_);
|
||||
db_ = nullptr;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -26,6 +27,26 @@ public:
|
||||
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||
};
|
||||
|
||||
// Everything one snapshot write needs, BY VALUE, so it can run on the history writer thread after the UI
|
||||
// state it came from has moved on (and after lockKey() zeroed the live key). Move-only; the private key
|
||||
// copy is wiped by writeSnapshot() and, for a job that never ran (superseded), by the destructor.
|
||||
struct SnapshotJob {
|
||||
std::string walletIdentity;
|
||||
std::array<unsigned char, 32> key{};
|
||||
int tipHeight = 0;
|
||||
std::string tipHash;
|
||||
std::vector<TransactionInfo> transactions;
|
||||
std::time_t updatedAt = 0;
|
||||
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||
|
||||
SnapshotJob() = default;
|
||||
SnapshotJob(const SnapshotJob&) = delete;
|
||||
SnapshotJob& operator=(const SnapshotJob&) = delete;
|
||||
SnapshotJob(SnapshotJob&&) = default;
|
||||
SnapshotJob& operator=(SnapshotJob&&) = default;
|
||||
~SnapshotJob();
|
||||
};
|
||||
|
||||
TransactionHistoryCache();
|
||||
explicit TransactionHistoryCache(std::string databasePath);
|
||||
~TransactionHistoryCache();
|
||||
@@ -41,8 +62,13 @@ public:
|
||||
bool ensureOpen();
|
||||
bool unlockWithPassphrase(const std::string& walletIdentity, const std::string& passphrase);
|
||||
void lockKey();
|
||||
bool hasKey() const { return key_ready_; }
|
||||
bool hasKey() const;
|
||||
bool isUnlockedFor(const std::string& walletIdentity) const;
|
||||
// Copy of the live key if it is unlocked for `walletIdentity` (keyOut untouched otherwise). Caller wipes it.
|
||||
bool captureKey(const std::string& walletIdentity, std::array<unsigned char, 32>& keyOut) const;
|
||||
// Non-reversible id of the key last installed. Survives lockKey(), so re-unlocking with the same
|
||||
// passphrase gives the same id and a passphrase change gives another. 0 = never unlocked.
|
||||
std::uint64_t keyId() const;
|
||||
|
||||
LoadResult load(const std::string& walletIdentity,
|
||||
int currentTipHeight,
|
||||
@@ -53,6 +79,9 @@ public:
|
||||
const std::vector<TransactionInfo>& transactions,
|
||||
std::time_t updatedAt,
|
||||
const std::unordered_map<std::string, int>& shieldedScanHeights = {});
|
||||
// Thread-safe snapshot write (the history writer thread; replace() wraps it): JSON + AEAD with no lock
|
||||
// held, then INSERT OR REPLACE + prune under the DB lock. Always wipes job.key. False for a keyless job.
|
||||
bool writeSnapshot(SnapshotJob&& job);
|
||||
void clearWallet(const std::string& walletIdentity);
|
||||
int snapshotCount();
|
||||
|
||||
@@ -60,12 +89,14 @@ private:
|
||||
bool exec(const char* sql);
|
||||
bool createSchema();
|
||||
std::vector<unsigned char> getOrCreateSalt(const std::string& walletHash);
|
||||
bool deriveKey(const std::string& passphrase,
|
||||
const std::vector<unsigned char>& salt);
|
||||
bool encryptPayload(const std::string& walletHash,
|
||||
const std::string& plainText,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipherText) const;
|
||||
static bool deriveKey(const std::string& passphrase,
|
||||
const std::vector<unsigned char>& salt,
|
||||
std::array<unsigned char, 32>& keyOut);
|
||||
static bool encryptPayload(const std::array<unsigned char, 32>& key,
|
||||
const std::string& walletHash,
|
||||
const std::string& plainText,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipherText);
|
||||
bool decryptPayload(const std::string& walletHash,
|
||||
const std::vector<unsigned char>& nonce,
|
||||
const std::vector<unsigned char>& cipherText,
|
||||
@@ -79,14 +110,21 @@ private:
|
||||
void clearWalletByHash(const std::string& walletHash);
|
||||
// Delete snapshot + salt rows for every wallet hash except the live one, bounding the
|
||||
// DB so generating a new address (which changes the hash) doesn't orphan history forever.
|
||||
void pruneOtherWallets(const std::string& keepWalletHash);
|
||||
// Also keeps `alsoKeepWalletHash` (the wallet unlocked now, if any): a delayed writer job for the previous
|
||||
// wallet must not delete the salt of the wallet just switched to.
|
||||
void pruneOtherWallets(const std::string& keepWalletHash, const std::string& alsoKeepWalletHash);
|
||||
void close();
|
||||
|
||||
// Lock order: db_mutex_ then key_mutex_. key_mutex_ is never held across I/O or the KDF. Only the UI
|
||||
// thread changes the key fields; the writer thread reads unlocked_wallet_hash_ (prune) under both locks.
|
||||
mutable std::recursive_mutex db_mutex_; // db_ and every SQL statement
|
||||
mutable std::mutex key_mutex_; // key_, key_ready_, unlocked_wallet_hash_, key_id_
|
||||
sqlite3* db_ = nullptr;
|
||||
std::string database_path_;
|
||||
std::array<unsigned char, 32> key_{};
|
||||
bool key_ready_ = false;
|
||||
std::string unlocked_wallet_hash_;
|
||||
std::uint64_t key_id_ = 0;
|
||||
};
|
||||
|
||||
} // namespace data
|
||||
|
||||
Reference in New Issue
Block a user