Skip to content

Commit ba616b9

Browse files
achow101ryanofskyfurszy
committed
wallet: Add GetPrefixCursor to DatabaseBatch
In order to get records beginning with a prefix, we will need a cursor specifically for that prefix. So add a GetPrefixCursor function and DatabaseCursor classes for dealing with those prefixes. Tested on each supported db engine. 1) Write two different key->value elements to db. 2) Create a new prefix cursor and walk-through every returned element, verifying that it gets parsed properly. 3) Try to move the cursor outside the filtered range: expect failure and flag complete=true. Co-Authored-By: Ryan Ofsky <[email protected]> Co-Authored-By: furszy <[email protected]>
1 parent 1d858b0 commit ba616b9

File tree

10 files changed

+239
-15
lines changed

10 files changed

+239
-15
lines changed

src/wallet/bdb.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,8 @@ void BerkeleyDatabase::ReloadDbEnv()
668668
env->ReloadDbEnv();
669669
}
670670

671-
BerkeleyCursor::BerkeleyCursor(BerkeleyDatabase& database, const BerkeleyBatch& batch)
671+
BerkeleyCursor::BerkeleyCursor(BerkeleyDatabase& database, const BerkeleyBatch& batch, Span<const std::byte> prefix)
672+
: m_key_prefix(prefix.begin(), prefix.end())
672673
{
673674
if (!database.m_db.get()) {
674675
throw std::runtime_error(STR_INTERNAL_BUG("BerkeleyDatabase does not exist"));
@@ -685,19 +686,30 @@ DatabaseCursor::Status BerkeleyCursor::Next(DataStream& ssKey, DataStream& ssVal
685686
{
686687
if (m_cursor == nullptr) return Status::FAIL;
687688
// Read at cursor
688-
SafeDbt datKey;
689+
SafeDbt datKey(m_key_prefix.data(), m_key_prefix.size());
689690
SafeDbt datValue;
690-
int ret = m_cursor->get(datKey, datValue, DB_NEXT);
691+
int ret = -1;
692+
if (m_first && !m_key_prefix.empty()) {
693+
ret = m_cursor->get(datKey, datValue, DB_SET_RANGE);
694+
} else {
695+
ret = m_cursor->get(datKey, datValue, DB_NEXT);
696+
}
697+
m_first = false;
691698
if (ret == DB_NOTFOUND) {
692699
return Status::DONE;
693700
}
694701
if (ret != 0) {
695702
return Status::FAIL;
696703
}
697704

705+
Span<const std::byte> raw_key = {AsBytePtr(datKey.get_data()), datKey.get_size()};
706+
if (!m_key_prefix.empty() && std::mismatch(raw_key.begin(), raw_key.end(), m_key_prefix.begin(), m_key_prefix.end()).second != m_key_prefix.end()) {
707+
return Status::DONE;
708+
}
709+
698710
// Convert to streams
699711
ssKey.clear();
700-
ssKey.write({AsBytePtr(datKey.get_data()), datKey.get_size()});
712+
ssKey.write(raw_key);
701713
ssValue.clear();
702714
ssValue.write({AsBytePtr(datValue.get_data()), datValue.get_size()});
703715
return Status::MORE;
@@ -716,6 +728,12 @@ std::unique_ptr<DatabaseCursor> BerkeleyBatch::GetNewCursor()
716728
return std::make_unique<BerkeleyCursor>(m_database, *this);
717729
}
718730

731+
std::unique_ptr<DatabaseCursor> BerkeleyBatch::GetNewPrefixCursor(Span<const std::byte> prefix)
732+
{
733+
if (!pdb) return nullptr;
734+
return std::make_unique<BerkeleyCursor>(m_database, *this, prefix);
735+
}
736+
719737
bool BerkeleyBatch::TxnBegin()
720738
{
721739
if (!pdb || activeTxn)

src/wallet/bdb.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,13 @@ class BerkeleyCursor : public DatabaseCursor
190190
{
191191
private:
192192
Dbc* m_cursor;
193+
std::vector<std::byte> m_key_prefix;
194+
bool m_first{true};
193195

194196
public:
195-
explicit BerkeleyCursor(BerkeleyDatabase& database, const BerkeleyBatch& batch);
197+
// Constructor for cursor for records matching the prefix
198+
// To match all records, an empty prefix may be provided.
199+
explicit BerkeleyCursor(BerkeleyDatabase& database, const BerkeleyBatch& batch, Span<const std::byte> prefix = {});
196200
~BerkeleyCursor() override;
197201

198202
Status Next(DataStream& key, DataStream& value) override;
@@ -229,6 +233,7 @@ class BerkeleyBatch : public DatabaseBatch
229233
void Close() override;
230234

231235
std::unique_ptr<DatabaseCursor> GetNewCursor() override;
236+
std::unique_ptr<DatabaseCursor> GetNewPrefixCursor(Span<const std::byte> prefix) override;
232237
bool TxnBegin() override;
233238
bool TxnCommit() override;
234239
bool TxnAbort() override;

src/wallet/db.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class DatabaseBatch
113113
virtual bool ErasePrefix(Span<const std::byte> prefix) = 0;
114114

115115
virtual std::unique_ptr<DatabaseCursor> GetNewCursor() = 0;
116+
virtual std::unique_ptr<DatabaseCursor> GetNewPrefixCursor(Span<const std::byte> prefix) = 0;
116117
virtual bool TxnBegin() = 0;
117118
virtual bool TxnCommit() = 0;
118119
virtual bool TxnAbort() = 0;

src/wallet/salvage.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DummyBatch : public DatabaseBatch
4343
void Close() override {}
4444

4545
std::unique_ptr<DatabaseCursor> GetNewCursor() override { return std::make_unique<DummyCursor>(); }
46+
std::unique_ptr<DatabaseCursor> GetNewPrefixCursor(Span<const std::byte> prefix) override { return GetNewCursor(); }
4647
bool TxnBegin() override { return true; }
4748
bool TxnCommit() override { return true; }
4849
bool TxnAbort() override { return true; }

src/wallet/sqlite.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <logging.h>
1010
#include <sync.h>
1111
#include <util/fs_helpers.h>
12+
#include <util/check.h>
1213
#include <util/strencodings.h>
1314
#include <util/translation.h>
1415
#include <wallet/db.h>
@@ -515,6 +516,7 @@ DatabaseCursor::Status SQLiteCursor::Next(DataStream& key, DataStream& value)
515516

516517
SQLiteCursor::~SQLiteCursor()
517518
{
519+
sqlite3_clear_bindings(m_cursor_stmt);
518520
sqlite3_reset(m_cursor_stmt);
519521
int res = sqlite3_finalize(m_cursor_stmt);
520522
if (res != SQLITE_OK) {
@@ -538,6 +540,48 @@ std::unique_ptr<DatabaseCursor> SQLiteBatch::GetNewCursor()
538540
return cursor;
539541
}
540542

543+
std::unique_ptr<DatabaseCursor> SQLiteBatch::GetNewPrefixCursor(Span<const std::byte> prefix)
544+
{
545+
if (!m_database.m_db) return nullptr;
546+
547+
// To get just the records we want, the SQL statement does a comparison of the binary data
548+
// where the data must be greater than or equal to the prefix, and less than
549+
// the prefix incremented by one (when interpreted as an integer)
550+
std::vector<std::byte> start_range(prefix.begin(), prefix.end());
551+
std::vector<std::byte> end_range(prefix.begin(), prefix.end());
552+
auto it = end_range.rbegin();
553+
for (; it != end_range.rend(); ++it) {
554+
if (*it == std::byte(std::numeric_limits<unsigned char>::max())) {
555+
*it = std::byte(0);
556+
continue;
557+
}
558+
*it = std::byte(std::to_integer<unsigned char>(*it) + 1);
559+
break;
560+
}
561+
if (it == end_range.rend()) {
562+
// If the prefix is all 0xff bytes, clear end_range as we won't need it
563+
end_range.clear();
564+
}
565+
566+
auto cursor = std::make_unique<SQLiteCursor>(start_range, end_range);
567+
if (!cursor) return nullptr;
568+
569+
const char* stmt_text = end_range.empty() ? "SELECT key, value FROM main WHERE key >= ?" :
570+
"SELECT key, value FROM main WHERE key >= ? AND key < ?";
571+
int res = sqlite3_prepare_v2(m_database.m_db, stmt_text, -1, &cursor->m_cursor_stmt, nullptr);
572+
if (res != SQLITE_OK) {
573+
throw std::runtime_error(strprintf(
574+
"SQLiteDatabase: Failed to setup cursor SQL statement: %s\n", sqlite3_errstr(res)));
575+
}
576+
577+
if (!BindBlobToStatement(cursor->m_cursor_stmt, 1, cursor->m_prefix_range_start, "prefix_start")) return nullptr;
578+
if (!end_range.empty()) {
579+
if (!BindBlobToStatement(cursor->m_cursor_stmt, 2, cursor->m_prefix_range_end, "prefix_end")) return nullptr;
580+
}
581+
582+
return cursor;
583+
}
584+
541585
bool SQLiteBatch::TxnBegin()
542586
{
543587
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) == 0) return false;

src/wallet/sqlite.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@ struct bilingual_str;
1515
namespace wallet {
1616
class SQLiteDatabase;
1717

18+
/** RAII class that provides a database cursor */
1819
class SQLiteCursor : public DatabaseCursor
1920
{
2021
public:
2122
sqlite3_stmt* m_cursor_stmt{nullptr};
23+
// Copies of the prefix things for the prefix cursor.
24+
// Prevents SQLite from accessing temp variables for the prefix things.
25+
std::vector<std::byte> m_prefix_range_start;
26+
std::vector<std::byte> m_prefix_range_end;
2227

2328
explicit SQLiteCursor() {}
29+
explicit SQLiteCursor(std::vector<std::byte> start_range, std::vector<std::byte> end_range)
30+
: m_prefix_range_start(std::move(start_range)),
31+
m_prefix_range_end(std::move(end_range))
32+
{}
2433
~SQLiteCursor() override;
2534

2635
Status Next(DataStream& key, DataStream& value) override;
@@ -57,6 +66,7 @@ class SQLiteBatch : public DatabaseBatch
5766
void Close() override;
5867

5968
std::unique_ptr<DatabaseCursor> GetNewCursor() override;
69+
std::unique_ptr<DatabaseCursor> GetNewPrefixCursor(Span<const std::byte> prefix) override;
6070
bool TxnBegin() override;
6171
bool TxnCommit() override;
6272
bool TxnAbort() override;

src/wallet/test/db_tests.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,56 @@
66

77
#include <test/util/setup_common.h>
88
#include <util/fs.h>
9+
#include <util/translation.h>
10+
#ifdef USE_BDB
911
#include <wallet/bdb.h>
12+
#endif
13+
#ifdef USE_SQLITE
14+
#include <wallet/sqlite.h>
15+
#endif
16+
#include <wallet/test/util.h>
17+
#include <wallet/walletutil.h> // for WALLET_FLAG_DESCRIPTORS
1018

1119
#include <fstream>
1220
#include <memory>
1321
#include <string>
1422

23+
inline std::ostream& operator<<(std::ostream& os, const std::pair<const SerializeData, SerializeData>& kv)
24+
{
25+
Span key{kv.first}, value{kv.second};
26+
os << "(\"" << std::string_view{reinterpret_cast<const char*>(key.data()), key.size()} << "\", \""
27+
<< std::string_view{reinterpret_cast<const char*>(key.data()), key.size()} << "\")";
28+
return os;
29+
}
30+
1531
namespace wallet {
32+
33+
static Span<const std::byte> StringBytes(std::string_view str)
34+
{
35+
return AsBytes<const char>({str.data(), str.size()});
36+
}
37+
38+
static SerializeData StringData(std::string_view str)
39+
{
40+
auto bytes = StringBytes(str);
41+
return SerializeData{bytes.begin(), bytes.end()};
42+
}
43+
44+
static void CheckPrefix(DatabaseBatch& batch, Span<const std::byte> prefix, MockableData expected)
45+
{
46+
std::unique_ptr<DatabaseCursor> cursor = batch.GetNewPrefixCursor(prefix);
47+
MockableData actual;
48+
while (true) {
49+
DataStream key, value;
50+
DatabaseCursor::Status status = cursor->Next(key, value);
51+
if (status == DatabaseCursor::Status::DONE) break;
52+
BOOST_CHECK(status == DatabaseCursor::Status::MORE);
53+
BOOST_CHECK(
54+
actual.emplace(SerializeData(key.begin(), key.end()), SerializeData(value.begin(), value.end())).second);
55+
}
56+
BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(), expected.begin(), expected.end());
57+
}
58+
1659
BOOST_FIXTURE_TEST_SUITE(db_tests, BasicTestingSetup)
1760

1861
static std::shared_ptr<BerkeleyEnvironment> GetWalletEnv(const fs::path& path, fs::path& database_filename)
@@ -78,5 +121,90 @@ BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_free_instance)
78121
BOOST_CHECK(env_2_a == env_2_b);
79122
}
80123

124+
static std::vector<std::unique_ptr<WalletDatabase>> TestDatabases(const fs::path& path_root)
125+
{
126+
std::vector<std::unique_ptr<WalletDatabase>> dbs;
127+
DatabaseOptions options;
128+
DatabaseStatus status;
129+
bilingual_str error;
130+
#ifdef USE_BDB
131+
dbs.emplace_back(MakeBerkeleyDatabase(path_root / "bdb", options, status, error));
132+
#endif
133+
#ifdef USE_SQLITE
134+
dbs.emplace_back(MakeSQLiteDatabase(path_root / "sqlite", options, status, error));
135+
#endif
136+
dbs.emplace_back(CreateMockableWalletDatabase());
137+
return dbs;
138+
}
139+
140+
BOOST_AUTO_TEST_CASE(db_cursor_prefix_range_test)
141+
{
142+
// Test each supported db
143+
for (const auto& database : TestDatabases(m_path_root)) {
144+
BOOST_ASSERT(database);
145+
146+
std::vector<std::string> prefixes = {"", "FIRST", "SECOND", "P\xfe\xff", "P\xff\x01", "\xff\xff"};
147+
148+
// Write elements to it
149+
std::unique_ptr<DatabaseBatch> handler = database->MakeBatch();
150+
for (unsigned int i = 0; i < 10; i++) {
151+
for (const auto& prefix : prefixes) {
152+
BOOST_CHECK(handler->Write(std::make_pair(prefix, i), i));
153+
}
154+
}
155+
156+
// Now read all the items by prefix and verify that each element gets parsed correctly
157+
for (const auto& prefix : prefixes) {
158+
DataStream s_prefix;
159+
s_prefix << prefix;
160+
std::unique_ptr<DatabaseCursor> cursor = handler->GetNewPrefixCursor(s_prefix);
161+
DataStream key;
162+
DataStream value;
163+
for (int i = 0; i < 10; i++) {
164+
DatabaseCursor::Status status = cursor->Next(key, value);
165+
BOOST_ASSERT(status == DatabaseCursor::Status::MORE);
166+
167+
std::string key_back;
168+
unsigned int i_back;
169+
key >> key_back >> i_back;
170+
BOOST_CHECK_EQUAL(key_back, prefix);
171+
172+
unsigned int value_back;
173+
value >> value_back;
174+
BOOST_CHECK_EQUAL(value_back, i_back);
175+
}
176+
177+
// Let's now read it once more, it should return DONE
178+
BOOST_CHECK(cursor->Next(key, value) == DatabaseCursor::Status::DONE);
179+
}
180+
}
181+
}
182+
183+
// Lower level DatabaseBase::GetNewPrefixCursor test, to cover cases that aren't
184+
// covered in the higher level test above. The higher level test uses
185+
// serialized strings which are prefixed with string length, so it doesn't test
186+
// truly empty prefixes or prefixes that begin with \xff
187+
BOOST_AUTO_TEST_CASE(db_cursor_prefix_byte_test)
188+
{
189+
const MockableData::value_type
190+
e{StringData(""), StringData("e")},
191+
p{StringData("prefix"), StringData("p")},
192+
ps{StringData("prefixsuffix"), StringData("ps")},
193+
f{StringData("\xff"), StringData("f")},
194+
fs{StringData("\xffsuffix"), StringData("fs")},
195+
ff{StringData("\xff\xff"), StringData("ff")},
196+
ffs{StringData("\xff\xffsuffix"), StringData("ffs")};
197+
for (const auto& database : TestDatabases(m_path_root)) {
198+
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
199+
for (const auto& [k, v] : {e, p, ps, f, fs, ff, ffs}) {
200+
batch->Write(MakeUCharSpan(k), MakeUCharSpan(v));
201+
}
202+
CheckPrefix(*batch, StringBytes(""), {e, p, ps, f, fs, ff, ffs});
203+
CheckPrefix(*batch, StringBytes("prefix"), {p, ps});
204+
CheckPrefix(*batch, StringBytes("\xff"), {f, fs, ff, ffs});
205+
CheckPrefix(*batch, StringBytes("\xff\xff"), {ff, ffs});
206+
}
207+
}
208+
81209
BOOST_AUTO_TEST_SUITE_END()
82210
} // namespace wallet

src/wallet/test/util.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ CTxDestination getNewDestination(CWallet& w, OutputType output_type)
9292
return *Assert(w.GetNewDestination(output_type, ""));
9393
}
9494

95+
// BytePrefix compares equality with other byte spans that begin with the same prefix.
96+
struct BytePrefix { Span<const std::byte> prefix; };
97+
bool operator<(BytePrefix a, Span<const std::byte> b) { return a.prefix < b.subspan(0, std::min(a.prefix.size(), b.size())); }
98+
bool operator<(Span<const std::byte> a, BytePrefix b) { return a.subspan(0, std::min(a.size(), b.prefix.size())) < b.prefix; }
99+
100+
MockableCursor::MockableCursor(const MockableData& records, bool pass, Span<const std::byte> prefix)
101+
{
102+
m_pass = pass;
103+
std::tie(m_cursor, m_cursor_end) = records.equal_range(BytePrefix{prefix});
104+
}
105+
95106
DatabaseCursor::Status MockableCursor::Next(DataStream& key, DataStream& value)
96107
{
97108
if (!m_pass) {
@@ -175,7 +186,7 @@ bool MockableBatch::ErasePrefix(Span<const std::byte> prefix)
175186
return true;
176187
}
177188

178-
std::unique_ptr<WalletDatabase> CreateMockableWalletDatabase(std::map<SerializeData, SerializeData> records)
189+
std::unique_ptr<WalletDatabase> CreateMockableWalletDatabase(MockableData records)
179190
{
180191
return std::make_unique<MockableDatabase>(records);
181192
}

0 commit comments

Comments
 (0)