walletdb: Refactor legacy wallet record loading into its own function

Instead of loading legacy wallet records as we come across them when
iterating the database, load them explicitly.

Exception handling for these records changes to a per-record type basis,
rather than globally. This results in some records now failing with a
critical error rather than a non-critical one.
This commit is contained in:
Andrew Chow 2022-04-12 14:27:39 -04:00 committed by Andrew Chow
parent 9e077d9b42
commit 30ab11c497
2 changed files with 283 additions and 153 deletions

View file

@ -301,10 +301,8 @@ class CWalletScanState {
public:
unsigned int nKeys{0};
unsigned int nCKeys{0};
unsigned int nWatchKeys{0};
unsigned int nKeyMeta{0};
unsigned int m_unknown_records{0};
bool fIsEncrypted{false};
bool fAnyUnordered{false};
std::vector<uint256> vWalletUpgrade;
std::map<OutputType, uint256> m_active_external_spks;
@ -312,10 +310,8 @@ public:
std::map<uint256, DescriptorCache> m_descriptor_caches;
std::map<std::pair<uint256, CKeyID>, CKey> m_descriptor_keys;
std::map<std::pair<uint256, CKeyID>, std::pair<CPubKey, std::vector<unsigned char>>> m_descriptor_crypt_keys;
std::map<uint160, CHDChain> m_hd_chains;
bool tx_corrupt{false};
bool descriptor_unknown{false};
bool unexpected_legacy_entry{false};
CWalletScanState() = default;
};
@ -477,10 +473,8 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
// Taking advantage of the fact that pair serialization
// is just the two items serialized one after the other
ssKey >> strType;
// Legacy entries in descriptor wallets are not allowed, abort immediately
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) && DBKeys::LEGACY_TYPES.count(strType) > 0) {
wss.unexpected_legacy_entry = true;
return false;
return true;
}
if (strType == DBKeys::NAME) {
std::string strAddress;
@ -545,97 +539,16 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
return false;
}
} else if (strType == DBKeys::WATCHS) {
wss.nWatchKeys++;
CScript script;
ssKey >> script;
uint8_t fYes;
ssValue >> fYes;
if (fYes == '1') {
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadWatchOnly(script);
}
} else if (strType == DBKeys::KEY) {
wss.nKeys++;
if (!LoadKey(pwallet, ssKey, ssValue, strErr)) return false;
} else if (strType == DBKeys::MASTER_KEY) {
if (!LoadEncryptionKey(pwallet, ssKey, ssValue, strErr)) return false;
} else if (strType == DBKeys::CRYPTED_KEY) {
wss.nCKeys++;
if (!LoadCryptedKey(pwallet, ssKey, ssValue, strErr)) return false;
wss.fIsEncrypted = true;
} else if (strType == DBKeys::KEYMETA) {
CPubKey vchPubKey;
ssKey >> vchPubKey;
CKeyMetadata keyMeta;
ssValue >> keyMeta;
wss.nKeyMeta++;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadKeyMetadata(vchPubKey.GetID(), keyMeta);
// Extract some CHDChain info from this metadata if it has any
if (keyMeta.nVersion >= CKeyMetadata::VERSION_WITH_HDDATA && !keyMeta.hd_seed_id.IsNull() && keyMeta.hdKeypath.size() > 0) {
// Get the path from the key origin or from the path string
// Not applicable when path is "s" or "m" as those indicate a seed
// See https://github.com/bitcoin/bitcoin/pull/12924
bool internal = false;
uint32_t index = 0;
if (keyMeta.hdKeypath != "s" && keyMeta.hdKeypath != "m") {
std::vector<uint32_t> path;
if (keyMeta.has_key_origin) {
// We have a key origin, so pull it from its path vector
path = keyMeta.key_origin.path;
} else {
// No key origin, have to parse the string
if (!ParseHDKeypath(keyMeta.hdKeypath, path)) {
strErr = "Error reading wallet database: keymeta with invalid HD keypath";
return false;
}
}
// Extract the index and internal from the path
// Path string is m/0'/k'/i'
// Path vector is [0', k', i'] (but as ints OR'd with the hardened bit
// k == 0 for external, 1 for internal. i is the index
if (path.size() != 3) {
strErr = "Error reading wallet database: keymeta found with unexpected path";
return false;
}
if (path[0] != 0x80000000) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000) for the element at index 0", path[0]);
return false;
}
if (path[1] != 0x80000000 && path[1] != (1 | 0x80000000)) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000 or 0x80000001) for the element at index 1", path[1]);
return false;
}
if ((path[2] & 0x80000000) == 0) {
strErr = strprintf("Unexpected path index of 0x%08x (expected to be greater than or equal to 0x80000000)", path[2]);
return false;
}
internal = path[1] == (1 | 0x80000000);
index = path[2] & ~0x80000000;
}
// Insert a new CHDChain, or get the one that already exists
auto ins = wss.m_hd_chains.emplace(keyMeta.hd_seed_id, CHDChain());
CHDChain& chain = ins.first->second;
if (ins.second) {
// For new chains, we want to default to VERSION_HD_BASE until we see an internal
chain.nVersion = CHDChain::VERSION_HD_BASE;
chain.seed_id = keyMeta.hd_seed_id;
}
if (internal) {
chain.nVersion = CHDChain::VERSION_HD_CHAIN_SPLIT;
chain.nInternalChainCounter = std::max(chain.nInternalChainCounter, index + 1);
} else {
chain.nExternalChainCounter = std::max(chain.nExternalChainCounter, index + 1);
}
}
} else if (strType == DBKeys::WATCHMETA) {
CScript script;
ssKey >> script;
CKeyMetadata keyMeta;
ssValue >> keyMeta;
wss.nKeyMeta++;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadScriptMetadata(CScriptID(script), keyMeta);
} else if (strType == DBKeys::DEFAULTKEY) {
// We don't want or need the default key, but if there is one set,
// we want to make sure that it is valid so that we can detect corruption
@ -646,22 +559,7 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
return false;
}
} else if (strType == DBKeys::POOL) {
int64_t nIndex;
ssKey >> nIndex;
CKeyPool keypool;
ssValue >> keypool;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadKeyPool(nIndex, keypool);
} else if (strType == DBKeys::CSCRIPT) {
uint160 hash;
ssKey >> hash;
CScript script;
ssValue >> script;
if (!pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadCScript(script))
{
strErr = "Error reading wallet database: LegacyScriptPubKeyMan::LoadCScript failed";
return false;
}
} else if (strType == DBKeys::ORDERPOSNEXT) {
ssValue >> pwallet->nOrderPosNext;
} else if (strType == DBKeys::DESTDATA) {
@ -684,7 +582,6 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
pwallet->LoadAddressReceiveRequest(dest, strKey.substr(2), strValue);
}
} else if (strType == DBKeys::HDCHAIN) {
if (!LoadHDChain(pwallet, ssValue, strErr)) return false;
} else if (strType == DBKeys::OLD_KEY) {
strErr = "Found unsupported 'wkey' record, try loading with version 0.18";
return false;
@ -803,7 +700,6 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
wss.nCKeys++;
wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey)));
wss.fIsEncrypted = true;
} else if (strType == DBKeys::LOCKED_UTXO) {
uint256 hash;
uint32_t n;
@ -855,6 +751,268 @@ static DBErrors LoadWalletFlags(CWallet* pwallet, DatabaseBatch& batch) EXCLUSIV
return DBErrors::LOAD_OK;
}
struct LoadResult
{
DBErrors m_result{DBErrors::LOAD_OK};
int m_records{0};
};
using LoadFunc = std::function<DBErrors(CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err)>;
static LoadResult LoadRecords(CWallet* pwallet, DatabaseBatch& batch, const std::string& key, LoadFunc load_func)
{
LoadResult result;
DataStream ssKey;
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
DataStream prefix;
prefix << key;
std::unique_ptr<DatabaseCursor> cursor = batch.GetNewPrefixCursor(prefix);
if (!cursor) {
pwallet->WalletLogPrintf("Error getting database cursor for '%s' records\n", key);
result.m_result = DBErrors::CORRUPT;
return result;
}
while (true) {
DatabaseCursor::Status status = cursor->Next(ssKey, ssValue);
if (status == DatabaseCursor::Status::DONE) {
break;
} else if (status == DatabaseCursor::Status::FAIL) {
pwallet->WalletLogPrintf("Error reading next '%s' record for wallet database\n", key);
result.m_result = DBErrors::CORRUPT;
return result;
}
std::string type;
ssKey >> type;
assert(type == key);
std::string error;
DBErrors record_res = load_func(pwallet, ssKey, ssValue, error);
if (record_res != DBErrors::LOAD_OK) {
pwallet->WalletLogPrintf("%s\n", error);
}
result.m_result = std::max(result.m_result, record_res);
++result.m_records;
}
return result;
}
static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch, int last_client) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
{
AssertLockHeld(pwallet->cs_wallet);
DBErrors result = DBErrors::LOAD_OK;
// Make sure descriptor wallets don't have any legacy records
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
for (const auto& type : DBKeys::LEGACY_TYPES) {
DataStream key;
CDataStream value(SER_DISK, CLIENT_VERSION);
DataStream prefix;
prefix << type;
std::unique_ptr<DatabaseCursor> cursor = batch.GetNewPrefixCursor(prefix);
if (!cursor) {
pwallet->WalletLogPrintf("Error getting database cursor for '%s' records\n", type);
return DBErrors::CORRUPT;
}
DatabaseCursor::Status status = cursor->Next(key, value);
if (status != DatabaseCursor::Status::DONE) {
pwallet->WalletLogPrintf("Error: Unexpected legacy entry found in descriptor wallet %s. The wallet might have been tampered with or created with malicious intent.\n", pwallet->GetName());
return DBErrors::UNEXPECTED_LEGACY_ENTRY;
}
}
return DBErrors::LOAD_OK;
}
// Load HD Chain
// Note: There should only be one HDCHAIN record with no data following the type
LoadResult hd_chain_res = LoadRecords(pwallet, batch, DBKeys::HDCHAIN,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
return LoadHDChain(pwallet, value, err) ? DBErrors:: LOAD_OK : DBErrors::CORRUPT;
});
result = std::max(result, hd_chain_res.m_result);
// Load unencrypted keys
LoadResult key_res = LoadRecords(pwallet, batch, DBKeys::KEY,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
return LoadKey(pwallet, key, value, err) ? DBErrors::LOAD_OK : DBErrors::CORRUPT;
});
result = std::max(result, key_res.m_result);
// Load encrypted keys
LoadResult ckey_res = LoadRecords(pwallet, batch, DBKeys::CRYPTED_KEY,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
return LoadCryptedKey(pwallet, key, value, err) ? DBErrors::LOAD_OK : DBErrors::CORRUPT;
});
result = std::max(result, ckey_res.m_result);
// Load scripts
LoadResult script_res = LoadRecords(pwallet, batch, DBKeys::CSCRIPT,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& strErr) {
uint160 hash;
key >> hash;
CScript script;
value >> script;
if (!pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadCScript(script))
{
strErr = "Error reading wallet database: LegacyScriptPubKeyMan::LoadCScript failed";
return DBErrors::NONCRITICAL_ERROR;
}
return DBErrors::LOAD_OK;
});
result = std::max(result, script_res.m_result);
// Check whether rewrite is needed
if (ckey_res.m_records > 0) {
// Rewrite encrypted wallets of versions 0.4.0 and 0.5.0rc:
if (last_client == 40000 || last_client == 50000) result = std::max(result, DBErrors::NEED_REWRITE);
}
// Load keymeta
std::map<uint160, CHDChain> hd_chains;
LoadResult keymeta_res = LoadRecords(pwallet, batch, DBKeys::KEYMETA,
[&hd_chains] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& strErr) {
CPubKey vchPubKey;
key >> vchPubKey;
CKeyMetadata keyMeta;
value >> keyMeta;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadKeyMetadata(vchPubKey.GetID(), keyMeta);
// Extract some CHDChain info from this metadata if it has any
if (keyMeta.nVersion >= CKeyMetadata::VERSION_WITH_HDDATA && !keyMeta.hd_seed_id.IsNull() && keyMeta.hdKeypath.size() > 0) {
// Get the path from the key origin or from the path string
// Not applicable when path is "s" or "m" as those indicate a seed
// See https://github.com/bitcoin/bitcoin/pull/12924
bool internal = false;
uint32_t index = 0;
if (keyMeta.hdKeypath != "s" && keyMeta.hdKeypath != "m") {
std::vector<uint32_t> path;
if (keyMeta.has_key_origin) {
// We have a key origin, so pull it from its path vector
path = keyMeta.key_origin.path;
} else {
// No key origin, have to parse the string
if (!ParseHDKeypath(keyMeta.hdKeypath, path)) {
strErr = "Error reading wallet database: keymeta with invalid HD keypath";
return DBErrors::NONCRITICAL_ERROR;
}
}
// Extract the index and internal from the path
// Path string is m/0'/k'/i'
// Path vector is [0', k', i'] (but as ints OR'd with the hardened bit
// k == 0 for external, 1 for internal. i is the index
if (path.size() != 3) {
strErr = "Error reading wallet database: keymeta found with unexpected path";
return DBErrors::NONCRITICAL_ERROR;
}
if (path[0] != 0x80000000) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000) for the element at index 0", path[0]);
return DBErrors::NONCRITICAL_ERROR;
}
if (path[1] != 0x80000000 && path[1] != (1 | 0x80000000)) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000 or 0x80000001) for the element at index 1", path[1]);
return DBErrors::NONCRITICAL_ERROR;
}
if ((path[2] & 0x80000000) == 0) {
strErr = strprintf("Unexpected path index of 0x%08x (expected to be greater than or equal to 0x80000000)", path[2]);
return DBErrors::NONCRITICAL_ERROR;
}
internal = path[1] == (1 | 0x80000000);
index = path[2] & ~0x80000000;
}
// Insert a new CHDChain, or get the one that already exists
auto [ins, inserted] = hd_chains.emplace(keyMeta.hd_seed_id, CHDChain());
CHDChain& chain = ins->second;
if (inserted) {
// For new chains, we want to default to VERSION_HD_BASE until we see an internal
chain.nVersion = CHDChain::VERSION_HD_BASE;
chain.seed_id = keyMeta.hd_seed_id;
}
if (internal) {
chain.nVersion = CHDChain::VERSION_HD_CHAIN_SPLIT;
chain.nInternalChainCounter = std::max(chain.nInternalChainCounter, index + 1);
} else {
chain.nExternalChainCounter = std::max(chain.nExternalChainCounter, index + 1);
}
}
return DBErrors::LOAD_OK;
});
result = std::max(result, keymeta_res.m_result);
// Set inactive chains
if (!hd_chains.empty()) {
LegacyScriptPubKeyMan* legacy_spkm = pwallet->GetLegacyScriptPubKeyMan();
if (legacy_spkm) {
for (const auto& [hd_seed_id, chain] : hd_chains) {
if (hd_seed_id != legacy_spkm->GetHDChain().seed_id) {
legacy_spkm->AddInactiveHDChain(chain);
}
}
} else {
pwallet->WalletLogPrintf("Inactive HD Chains found but no Legacy ScriptPubKeyMan\n");
result = DBErrors::CORRUPT;
}
}
// Load watchonly scripts
LoadResult watch_script_res = LoadRecords(pwallet, batch, DBKeys::WATCHS,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
CScript script;
key >> script;
uint8_t fYes;
value >> fYes;
if (fYes == '1') {
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadWatchOnly(script);
}
return DBErrors::LOAD_OK;
});
result = std::max(result, watch_script_res.m_result);
// Load watchonly meta
LoadResult watch_meta_res = LoadRecords(pwallet, batch, DBKeys::WATCHMETA,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
CScript script;
key >> script;
CKeyMetadata keyMeta;
value >> keyMeta;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadScriptMetadata(CScriptID(script), keyMeta);
return DBErrors::LOAD_OK;
});
result = std::max(result, watch_meta_res.m_result);
// Load keypool
LoadResult pool_res = LoadRecords(pwallet, batch, DBKeys::POOL,
[] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) {
int64_t nIndex;
key >> nIndex;
CKeyPool keypool;
value >> keypool;
pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadKeyPool(nIndex, keypool);
return DBErrors::LOAD_OK;
});
result = std::max(result, pool_res.m_result);
if (result <= DBErrors::NONCRITICAL_ERROR) {
// Only do logging and time first key update if there were no critical errors
pwallet->WalletLogPrintf("Legacy Wallet Keys: %u plaintext, %u encrypted, %u w/ metadata, %u total.\n",
key_res.m_records, ckey_res.m_records, keymeta_res.m_records, key_res.m_records + ckey_res.m_records);
// nTimeFirstKey is only reliable if all keys have metadata
if (pwallet->IsLegacy() && (key_res.m_records + ckey_res.m_records + watch_script_res.m_records) != (keymeta_res.m_records + watch_meta_res.m_records)) {
auto spk_man = pwallet->GetOrCreateLegacyScriptPubKeyMan();
if (spk_man) {
LOCK(spk_man->cs_KeyStore);
spk_man->UpdateTimeFirstKey(1);
}
}
}
return result;
}
DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
{
CWalletScanState wss;
@ -883,6 +1041,9 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
}
#endif
// Load legacy wallet keys
result = std::max(LoadLegacyWalletRecords(pwallet, *m_batch, last_client), result);
// Get cursor
std::unique_ptr<DatabaseCursor> cursor = m_batch->GetNewCursor();
if (!cursor)
@ -909,17 +1070,9 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
std::string strType, strErr;
if (!ReadKeyValue(pwallet, ssKey, ssValue, wss, strType, strErr))
{
if (wss.unexpected_legacy_entry) {
strErr = strprintf("Error: Unexpected legacy entry found in descriptor wallet %s. ", pwallet->GetName());
strErr += "The wallet might have been tampered with or created with malicious intent.";
pwallet->WalletLogPrintf("%s\n", strErr);
return DBErrors::UNEXPECTED_LEGACY_ENTRY;
}
// losing keys is considered a catastrophic error, anything else
// we assume the user can live with:
if (strType == DBKeys::KEY ||
strType == DBKeys::MASTER_KEY ||
strType == DBKeys::CRYPTED_KEY ||
if (strType == DBKeys::MASTER_KEY ||
strType == DBKeys::DEFAULTKEY) {
result = DBErrors::CORRUPT;
} else if (wss.tx_corrupt) {
@ -946,6 +1099,8 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
pwallet->WalletLogPrintf("%s\n", strErr);
}
} catch (...) {
// Exceptions that can be ignored or treated as non-critical are handled by the individual loading functions.
// Any uncaught exceptions will be caught here and treated as critical.
result = DBErrors::CORRUPT;
}
@ -988,22 +1143,9 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
pwallet->WalletLogPrintf("Keys: %u plaintext, %u encrypted, %u w/ metadata, %u total. Unknown wallet records: %u\n",
wss.nKeys, wss.nCKeys, wss.nKeyMeta, wss.nKeys + wss.nCKeys, wss.m_unknown_records);
// nTimeFirstKey is only reliable if all keys have metadata
if (pwallet->IsLegacy() && (wss.nKeys + wss.nCKeys + wss.nWatchKeys) != wss.nKeyMeta) {
auto spk_man = pwallet->GetOrCreateLegacyScriptPubKeyMan();
if (spk_man) {
LOCK(spk_man->cs_KeyStore);
spk_man->UpdateTimeFirstKey(1);
}
}
for (const uint256& hash : wss.vWalletUpgrade)
WriteTx(pwallet->mapWallet.at(hash));
// Rewrite encrypted wallets of versions 0.4.0 and 0.5.0rc:
if (wss.fIsEncrypted && (last_client == 40000 || last_client == 50000))
return DBErrors::NEED_REWRITE;
if (!has_last_client || last_client != CLIENT_VERSION) // Update
m_batch->Write(DBKeys::VERSION, CLIENT_VERSION);
@ -1026,20 +1168,6 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
result = DBErrors::CORRUPT;
}
// Set the inactive chain
if (wss.m_hd_chains.size() > 0) {
LegacyScriptPubKeyMan* legacy_spkm = pwallet->GetLegacyScriptPubKeyMan();
if (!legacy_spkm) {
pwallet->WalletLogPrintf("Inactive HD Chains found but no Legacy ScriptPubKeyMan\n");
return DBErrors::CORRUPT;
}
for (const auto& chain_pair : wss.m_hd_chains) {
if (chain_pair.first != pwallet->GetLegacyScriptPubKeyMan()->GetHDChain().seed_id) {
pwallet->GetLegacyScriptPubKeyMan()->AddInactiveHDChain(chain_pair.second);
}
}
}
return result;
}

View file

@ -42,19 +42,21 @@ struct WalletContext;
static const bool DEFAULT_FLUSHWALLET = true;
/** Error statuses for the wallet database */
enum class DBErrors
/** Error statuses for the wallet database.
* Values are in order of severity. When multiple errors occur, the most severe (highest value) will be returned.
*/
enum class DBErrors : int
{
LOAD_OK,
CORRUPT,
NONCRITICAL_ERROR,
TOO_NEW,
EXTERNAL_SIGNER_SUPPORT_REQUIRED,
LOAD_FAIL,
NEED_REWRITE,
NEED_RESCAN,
UNKNOWN_DESCRIPTOR,
UNEXPECTED_LEGACY_ENTRY
LOAD_OK = 0,
NEED_RESCAN = 1,
NEED_REWRITE = 2,
EXTERNAL_SIGNER_SUPPORT_REQUIRED = 3,
NONCRITICAL_ERROR = 4,
TOO_NEW = 5,
UNKNOWN_DESCRIPTOR = 6,
LOAD_FAIL = 7,
UNEXPECTED_LEGACY_ENTRY = 8,
CORRUPT = 9,
};
namespace DBKeys {