Merge bitcoin/bitcoin#26606: wallet: Implement independent BDB parser
d51fbab4b3wallet, test: Be able to always swap BDB endianness (Ava Chow)0b753156cetest: Test bdb_ro dump of wallet without reset LSNs (Ava Chow)c1984f1282test: Test dumping dbs with overflow pages (Ava Chow)fd7b16e391test: Test dumps of other endian BDB files (Ava Chow)6ace3e953fbdb: Be able to make byteswapped databases (Ava Chow)d9878903fbError if LSNs are not reset (Ava Chow)4d7a3ae78eBerkeley RO Database fuzz test (TheCharlatan)3568dce9e9tests: Add BerkeleyRO to db prefix tests (Ava Chow)70cfbfdadfwallettool: Optionally use BERKELEY_RO as format when dumping BDB wallets (Ava Chow)dd57713f6eAdd MakeBerkeleyRODatabase (Ava Chow)6e50bee67dImplement handling of other endianness in BerkeleyRODatabase (Ava Chow)cdd61c9cc1wallet: implement independent BDB deserializer in BerkeleyRODatabase (Ava Chow)ecba230979wallet: implement BerkeleyRODatabase::Backup (Ava Chow)0c8e728476wallet: implement BerkeleyROBatch (Ava Chow)756ff9b478wallet: add dummy BerkeleyRODatabase and BerkeleyROBatch classes (Ava Chow)ca18aea5c4Add AutoFile::seek and tell (Ava Chow) Pull request description: Split from #26596 This PR adds `BerkeleyRODatabase` which is an independent implementation of a BDB file parser. It provides read only access to a BDB file, and can therefore be used as a read only database backend for wallets. This will be used for dumping legacy wallet records and migrating legacy wallets without the need for BDB itself. Wallettool's `dump` command is changed to use `BerkeleyRODatabase` instead of `BerkeleyDatabase` (and `CWallet` itself) to demonstrate that this parser works and to test it against the existing wallettool functional tests. ACKs for top commit: josibake: reACK https://github.com/bitcoin/bitcoin/commit/d51fbab4b32d56765e8faab6ad01245fb259b0ca TheCharlatan: Re-ACKd51fbab4b3furszy: reACKd51fbab4b3laanwj: re-ACKd51fbab4b3theStack: ACKd51fbab4b3Tree-SHA512: 1e7b97edf223b2974eed2e9eac1179fc82bb6359e0a66b7d2a0c8b9fa515eae9ea036f1edf7c76cdab2e75ad994962b134b41056ccfbc33b8d54f0859e86657b
This commit is contained in:
23 files changed
+1270
-19
No files matched your search
@@ -16,6 +16,7 @@
|
||||
#ifdef USE_SQLITE
|
||||
#include <wallet/sqlite.h>
|
||||
#endif
|
||||
#include <wallet/migrate.h>
|
||||
#include <wallet/test/util.h>
|
||||
#include <wallet/walletutil.h> // for WALLET_FLAG_DESCRIPTORS
|
||||
|
||||
@@ -132,6 +133,8 @@ static std::vector<std::unique_ptr<WalletDatabase>> TestDatabases(const fs::path
|
||||
bilingual_str error;
|
||||
#ifdef USE_BDB
|
||||
dbs.emplace_back(MakeBerkeleyDatabase(path_root / "bdb", options, status, error));
|
||||
// Needs BDB to make the DB to read
|
||||
dbs.emplace_back(std::make_unique<BerkeleyRODatabase>(BDBDataFile(path_root / "bdb"), /*open=*/false));
|
||||
#endif
|
||||
#ifdef USE_SQLITE
|
||||
dbs.emplace_back(MakeSQLiteDatabase(path_root / "sqlite", options, status, error));
|
||||
@@ -146,11 +149,16 @@ BOOST_AUTO_TEST_CASE(db_cursor_prefix_range_test)
|
||||
for (const auto& database : TestDatabases(m_path_root)) {
|
||||
std::vector<std::string> prefixes = {"", "FIRST", "SECOND", "P\xfe\xff", "P\xff\x01", "\xff\xff"};
|
||||
|
||||
// Write elements to it
|
||||
std::unique_ptr<DatabaseBatch> handler = Assert(database)->MakeBatch();
|
||||
for (unsigned int i = 0; i < 10; i++) {
|
||||
for (const auto& prefix : prefixes) {
|
||||
BOOST_CHECK(handler->Write(std::make_pair(prefix, i), i));
|
||||
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
|
||||
// For BerkeleyRO, open the file now. This must happen after BDB has written to the file
|
||||
database->Open();
|
||||
} else {
|
||||
// Write elements to it if not berkeleyro
|
||||
for (unsigned int i = 0; i < 10; i++) {
|
||||
for (const auto& prefix : prefixes) {
|
||||
BOOST_CHECK(handler->Write(std::make_pair(prefix, i), i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +186,8 @@ BOOST_AUTO_TEST_CASE(db_cursor_prefix_range_test)
|
||||
// Let's now read it once more, it should return DONE
|
||||
BOOST_CHECK(cursor->Next(key, value) == DatabaseCursor::Status::DONE);
|
||||
}
|
||||
handler.reset();
|
||||
database->Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,13 +207,23 @@ BOOST_AUTO_TEST_CASE(db_cursor_prefix_byte_test)
|
||||
ffs{StringData("\xff\xffsuffix"), StringData("ffs")};
|
||||
for (const auto& database : TestDatabases(m_path_root)) {
|
||||
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
|
||||
for (const auto& [k, v] : {e, p, ps, f, fs, ff, ffs}) {
|
||||
batch->Write(Span{k}, Span{v});
|
||||
|
||||
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
|
||||
// For BerkeleyRO, open the file now. This must happen after BDB has written to the file
|
||||
database->Open();
|
||||
} else {
|
||||
// Write elements to it if not berkeleyro
|
||||
for (const auto& [k, v] : {e, p, ps, f, fs, ff, ffs}) {
|
||||
batch->Write(Span{k}, Span{v});
|
||||
}
|
||||
}
|
||||
|
||||
CheckPrefix(*batch, StringBytes(""), {e, p, ps, f, fs, ff, ffs});
|
||||
CheckPrefix(*batch, StringBytes("prefix"), {p, ps});
|
||||
CheckPrefix(*batch, StringBytes("\xff"), {f, fs, ff, ffs});
|
||||
CheckPrefix(*batch, StringBytes("\xff\xff"), {ff, ffs});
|
||||
batch.reset();
|
||||
database->Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +233,10 @@ BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
|
||||
// To simulate the behavior, record overwrites are disallowed, and the test verifies
|
||||
// that the database remains active after failing to store an existing record.
|
||||
for (const auto& database : TestDatabases(m_path_root)) {
|
||||
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
|
||||
// Skip this test if BerkeleyRO
|
||||
continue;
|
||||
}
|
||||
// Write original record
|
||||
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
|
||||
std::string key = "key";
|
||||
@@ -241,6 +265,10 @@ BOOST_AUTO_TEST_CASE(erase_prefix)
|
||||
auto make_key = [](std::string type, std::string id) { return std::make_pair(type, id); };
|
||||
|
||||
for (const auto& database : TestDatabases(m_path_root)) {
|
||||
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
|
||||
// Skip this test if BerkeleyRO
|
||||
continue;
|
||||
}
|
||||
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
|
||||
|
||||
// Write two entries with the same key type prefix, a third one with a different prefix
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2023 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <config/bitcoin-config.h> // IWYU pragma: keep
|
||||
#include <test/fuzz/FuzzedDataProvider.h>
|
||||
#include <test/fuzz/fuzz.h>
|
||||
#include <test/fuzz/util.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/fs.h>
|
||||
#include <util/time.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/bdb.h>
|
||||
#include <wallet/db.h>
|
||||
#include <wallet/dump.h>
|
||||
#include <wallet/migrate.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
using wallet::DatabaseOptions;
|
||||
using wallet::DatabaseStatus;
|
||||
|
||||
namespace {
|
||||
TestingSetup* g_setup;
|
||||
} // namespace
|
||||
|
||||
void initialize_wallet_bdb_parser()
|
||||
{
|
||||
static auto testing_setup = MakeNoLogFileContext<TestingSetup>();
|
||||
g_setup = testing_setup.get();
|
||||
}
|
||||
|
||||
FUZZ_TARGET(wallet_bdb_parser, .init = initialize_wallet_bdb_parser)
|
||||
{
|
||||
const auto wallet_path = g_setup->m_args.GetDataDirNet() / "fuzzed_wallet.dat";
|
||||
|
||||
{
|
||||
AutoFile outfile{fsbridge::fopen(wallet_path, "wb")};
|
||||
outfile << Span{buffer};
|
||||
}
|
||||
|
||||
const DatabaseOptions options{};
|
||||
DatabaseStatus status;
|
||||
bilingual_str error;
|
||||
|
||||
fs::path bdb_ro_dumpfile{g_setup->m_args.GetDataDirNet() / "fuzzed_dumpfile_bdb_ro.dump"};
|
||||
if (fs::exists(bdb_ro_dumpfile)) { // Writing into an existing dump file will throw an exception
|
||||
remove(bdb_ro_dumpfile);
|
||||
}
|
||||
g_setup->m_args.ForceSetArg("-dumpfile", fs::PathToString(bdb_ro_dumpfile));
|
||||
|
||||
#ifdef USE_BDB
|
||||
bool bdb_ro_err = false;
|
||||
bool bdb_ro_pgno_err = false;
|
||||
#endif
|
||||
auto db{MakeBerkeleyRODatabase(wallet_path, options, status, error)};
|
||||
if (db) {
|
||||
assert(DumpWallet(g_setup->m_args, *db, error));
|
||||
} else {
|
||||
#ifdef USE_BDB
|
||||
bdb_ro_err = true;
|
||||
#endif
|
||||
if (error.original == "AutoFile::ignore: end of file: iostream error" ||
|
||||
error.original == "AutoFile::read: end of file: iostream error" ||
|
||||
error.original == "Not a BDB file" ||
|
||||
error.original == "Unsupported BDB data file version number" ||
|
||||
error.original == "Unexpected page type, should be 9 (BTree Metadata)" ||
|
||||
error.original == "Unexpected database flags, should only be 0x20 (subdatabases)" ||
|
||||
error.original == "Unexpected outer database root page type" ||
|
||||
error.original == "Unexpected number of entries in outer database root page" ||
|
||||
error.original == "Subdatabase has an unexpected name" ||
|
||||
error.original == "Subdatabase page number has unexpected length" ||
|
||||
error.original == "Unexpected inner database page type" ||
|
||||
error.original == "Unknown record type in records page" ||
|
||||
error.original == "Unknown record type in internal page" ||
|
||||
error.original == "Unexpected page size" ||
|
||||
error.original == "Unexpected page type" ||
|
||||
error.original == "Page number mismatch" ||
|
||||
error.original == "Bad btree level" ||
|
||||
error.original == "Bad page size" ||
|
||||
error.original == "File size is not a multiple of page size" ||
|
||||
error.original == "Meta page number mismatch") {
|
||||
// Do nothing
|
||||
} else if (error.original == "Subdatabase last page is greater than database last page" ||
|
||||
error.original == "Page number is greater than database last page" ||
|
||||
error.original == "Page number is greater than subdatabase last page" ||
|
||||
error.original == "Last page number could not fit in file") {
|
||||
#ifdef USE_BDB
|
||||
bdb_ro_pgno_err = true;
|
||||
#endif
|
||||
} else {
|
||||
throw std::runtime_error(error.original);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_BDB
|
||||
// Try opening with BDB
|
||||
fs::path bdb_dumpfile{g_setup->m_args.GetDataDirNet() / "fuzzed_dumpfile_bdb.dump"};
|
||||
if (fs::exists(bdb_dumpfile)) { // Writing into an existing dump file will throw an exception
|
||||
remove(bdb_dumpfile);
|
||||
}
|
||||
g_setup->m_args.ForceSetArg("-dumpfile", fs::PathToString(bdb_dumpfile));
|
||||
|
||||
try {
|
||||
auto db{MakeBerkeleyDatabase(wallet_path, options, status, error)};
|
||||
if (bdb_ro_err && !db) {
|
||||
return;
|
||||
}
|
||||
assert(db);
|
||||
if (bdb_ro_pgno_err) {
|
||||
// BerkeleyRO will throw on opening for errors involving bad page numbers, but BDB does not.
|
||||
// Ignore those.
|
||||
return;
|
||||
}
|
||||
assert(!bdb_ro_err);
|
||||
assert(DumpWallet(g_setup->m_args, *db, error));
|
||||
} catch (const std::runtime_error& e) {
|
||||
if (bdb_ro_err) return;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Make sure the dumpfiles match
|
||||
if (fs::exists(bdb_ro_dumpfile) && fs::exists(bdb_dumpfile)) {
|
||||
std::ifstream bdb_ro_dump(bdb_ro_dumpfile, std::ios_base::binary | std::ios_base::in);
|
||||
std::ifstream bdb_dump(bdb_dumpfile, std::ios_base::binary | std::ios_base::in);
|
||||
assert(std::equal(
|
||||
std::istreambuf_iterator<char>(bdb_ro_dump.rdbuf()),
|
||||
std::istreambuf_iterator<char>(),
|
||||
std::istreambuf_iterator<char>(bdb_dump.rdbuf())));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -93,11 +93,6 @@ CTxDestination getNewDestination(CWallet& w, OutputType output_type)
|
||||
return *Assert(w.GetNewDestination(output_type, ""));
|
||||
}
|
||||
|
||||
// BytePrefix compares equality with other byte spans that begin with the same prefix.
|
||||
struct BytePrefix { Span<const std::byte> prefix; };
|
||||
bool operator<(BytePrefix a, Span<const std::byte> b) { return a.prefix < b.subspan(0, std::min(a.prefix.size(), b.size())); }
|
||||
bool operator<(Span<const std::byte> a, BytePrefix b) { return a.subspan(0, std::min(a.size(), b.prefix.size())) < b.prefix; }
|
||||
|
||||
MockableCursor::MockableCursor(const MockableData& records, bool pass, Span<const std::byte> prefix)
|
||||
{
|
||||
m_pass = pass;
|
||||
|
||||
Reference in new issue
Block a user