achow101
commented at 2:42 am on May 27, 2020:
member
This PR adds a new class SQLiteDatabase which is a subclass of WalletDatabase. This provides access to a SQLite database that is used to store the wallet records. To keep compatibility with BDB and to complexity of the change down, we don’t make use of many SQLite’s features. We use it strictly as a key-value store. We create a table main which has two columns, key and value both with the type blob.
For new descriptor wallets, we will create a SQLiteDatabase instead of a BerkeleyDatabase. There is no requirement that all SQLite wallets are descriptor wallets, nor is there a requirement that all descriptor wallets be SQLite wallets. This allows for existing descriptor wallets to work as well as keeping open the option to migrate existing wallets to SQLite.
We keep the name wallet.dat for SQLite wallets. We are able to determine which database type to use by searching for specific magic bytes in the wallet.dat file. SQLite begins it’s files with a null terminated string SQLite format 3. BDB has 0x00053162 at byte 12 (note that the byte order of this integer depends on the system endianness). So when we see that there is a wallet.dat file that we want to open, we check for the magic bytes to determine which database system to use.
I decided to keep the wallet.dat naming to keep things like backup script to continue to function as they won’t need to be modified to look for a different file name. It also simplifies a couple of things in the implementation and the tests as wallet.dat is something that is specifically being looked for. If we don’t want this behavior, then I do have another branch which creates wallet.sqlite files instead, but I find that this direction is easier.
fanquake added the label
Wallet
on May 27, 2020
achow101 force-pushed
on May 27, 2020
achow101 force-pushed
on May 27, 2020
achow101
commented at 3:19 am on May 27, 2020:
member
I’ve dropped the amalgamation file
achow101 force-pushed
on May 27, 2020
achow101 force-pushed
on May 27, 2020
achow101 force-pushed
on May 27, 2020
jonasschnelli
commented at 3:54 pm on May 27, 2020:
contributor
Pretty amazing work! Thanks for doing this.
For testing purposes, would it make sense to add logdb (#5686, simple implementation) in order to test and benchmark?
Concept ACK on a BDB replacement for descriptor wallets.
Still unsure wether sqlite or an internal format should be chosen. Maybe a comparison(-matrix) of internal vs. sqlite could be done?
As for concrete implementation steps, maybe it would make sense to PR the DB flexibility first, then additional storage engines later.
achow101 force-pushed
on May 27, 2020
achow101
commented at 4:25 pm on May 27, 2020:
member
For testing purposes, would it make sense to add logdb (#5686, simple implementation) in order to test and benchmark?
I don’t think it really makes sense to add a database system that we aren’t going to use.
Still unsure wether sqlite or an internal format should be chosen. Maybe a comparison(-matrix) of internal vs. sqlite could be done?
I think there’s two primary reasons to choose sqlite over an internal format.
Review and implementation are much simpler The library already exists so implementation just means correctly using the API. Reviewers won’t have to review a file format implementation and convince themselves that that format won’t corrupt and is robust.
Better guarantees of consistency and non-corruption. SQLite is very well tested and very widely used. I think they are able better guarantee that data will get written, won’t get lost, and won’t get corrupted, than we would be able to with an internal format.
As for concrete implementation steps, maybe it would make sense to PR the DB flexibility first, then additional storage engines later.
#18971 does the DB class stuff that gives us this flexibility. This PR is adding in the storage engine and the logic for CWallet to choose which storage to use.
achow101 force-pushed
on May 27, 2020
achow101 force-pushed
on May 27, 2020
practicalswift
commented at 7:21 pm on May 27, 2020:
contributor
Concept ACK
Nice work! Very readable code!
DrahtBot
commented at 7:50 pm on May 27, 2020:
member
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Conflicts
Reviewers, this pull request conflicts with the following ones:
#20094 (wallet: Unify wallet directory lock error message by hebasto)
#19502 (Bugfix: Wallet: Soft-fail exceptions within ListWalletDir file checks by luke-jr)
#19419 (wallet: let Listwalletdir do not iterate through our blocksdata. by Saibato)
#19101 (refactor: remove ::vpwallets and related global variables by ryanofsky)
#18095 (Fix crashes and infinite loop in ListWalletDir() by uhliksk)
#18077 (net: Add NAT-PMP port forwarding support by hebasto)
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
achow101 force-pushed
on May 27, 2020
achow101 force-pushed
on May 27, 2020
laanwj
commented at 10:41 am on May 28, 2020:
member
CONCEPT ACK
:tada: :partying_face: :tada:
Very happy to move on from BerkeleyDB and I’ve always liked sqlite as a versatile but still minimalistic replacement.
achow101 force-pushed
on May 28, 2020
achow101 force-pushed
on May 28, 2020
Sjors
commented at 3:28 pm on June 1, 2020:
member
Concept ACK. I’m able to build and run the test suite (including feature_backwards_compatibility.py) on macOS 10.15.4 with Homebrew brew install sqlite3 (don’t forget to add). I’m also able to build with /depends. I’m able to load an existing descriptor wallet (bdb) and create a new one.
Is there a particular reason to stick to .dat as the file extension, rather than .sqlite? If you do the latter, listwalletdir and the Open Wallet GUI need a trivial change.
Would it make sense to switch some of the records over to a format that’s more easy to inspect with a SQLite viewer? As well as use tables like “transactions”? Or would that make this PR far too complicated?
achow101
commented at 3:58 pm on June 1, 2020:
member
Is there a particular reason to stick to .dat as the file extension, rather than .sqlite? If you do the latter, listwalletdir and the Open Wallet GUI need a trivial change.
There are 2 reasons. The first is that it’s easier on review on implementation to stick to one filename. As you mentioned, if I make it .sqlite, listwalletdir and other places need to be changed. There are several places throughout the codebase where we specifically look for wallet.dat and not changing those to also use wallet.sqlite could be very bad. Additionally, some of those places may need to know whether they are looking for wallet.dat or wallet.sqlite so they would need access to whether a wallet is a descriptor wallet. They may need to know what kind of storage to look for which exposes more information than we currently already do. It’s much simpler to just keep it wallet.dat. This avoids issues where the wrong filename could be used and makes review simpler.
The second reason is that there are already lots of tooling, documentation, and discussion that use wallet.dat. Things like backup scripts or instructions telling people how to backup their wallet won’t be invalidated as we keep the same naming. For the most part, the end user doesn’t care about how the data is being stored within the file, they just need to know to preserve the wallet.dat file. So by keeping the naming, all of these things stay the same and make it less likely for people to lose their money.
There are, of course, a few cases where tooling does need to be updated because of the format change. But this tooling is all for manual inspection of the wallet.dat file and most users aren’t using that tooling.
Would it make sense to switch some of the records over to a format that’s more easy to inspect with a SQLite viewer? As well as use tables like “transactions”? Or would that make this PR far too complicated?
I would like to do that in the future. But I think that is too complicated for right now.
achow101 force-pushed
on Jun 1, 2020
achow101 force-pushed
on Jun 2, 2020
achow101 force-pushed
on Jun 2, 2020
luke-jr
commented at 6:03 pm on June 3, 2020:
member
I don’t think it really makes sense to add a database system that we aren’t going to use.
Maybe it’s time to use logdb.
Review and implementation are much simpler
Realistically, this should be phrased “review and implementation are behind closed doors by another team, and non-transparent”.
While SQLite has a free license, it is not open development. I’m not sure if their review standards are even documented.
I’ve always liked sqlite as a versatile but still minimalistic
Not sure SQLite counts as minimalistic… :p
achow101 force-pushed
on Jun 9, 2020
jamesob
commented at 8:33 pm on June 11, 2020:
member
Concept ACK. Awesome.
achow101 force-pushed
on Jun 16, 2020
achow101 force-pushed
on Jun 16, 2020
achow101 force-pushed
on Jun 18, 2020
achow101 force-pushed
on Jun 19, 2020
achow101 force-pushed
on Jun 19, 2020
meshcollider added this to the "Design" column in a project
achow101 force-pushed
on Jun 20, 2020
achow101 force-pushed
on Jun 22, 2020
achow101 force-pushed
on Jun 23, 2020
achow101 force-pushed
on Jul 1, 2020
achow101 force-pushed
on Jul 6, 2020
achow101 force-pushed
on Jul 9, 2020
achow101 force-pushed
on Jul 14, 2020
achow101 force-pushed
on Jul 23, 2020
achow101 marked this as ready for review
on Jul 23, 2020
achow101
commented at 3:50 am on July 23, 2020:
member
Since #19334 has been merged, this is now ready for review.
in
src/wallet/walletutil.cpp:138
in
6e7ef52e35outdated
119+ if (path_type == fs::directory_file || (path_type == fs::symlink_file && fs::is_directory(path))) {
120 path = fs::absolute("wallet.dat", m_path);
121+ return fs::symlink_status(path).type() != fs::file_not_found;
122 }
123- return fs::symlink_status(path).type() != fs::file_not_found;
124+ // Something exists here but we don't know what it is... Just say something exists so an error can be caught later
In commit “Change WalletLocation::Exists to check for wallet file existence” (6e7ef52e359e55bc82a30207a701995f0ca255d7)
Minor: I can’t really figure out what this commit and also the previous commit “wallet: Don’t Verify if database location doesn’t exist” (b64e40310742e3975ae704a801ccafa73d8617bf) are doing. Changes seem harmless, but they are making code more complicated. It would be good if commit messages mentioned motivations in commits like these where the motivations aren’t obvious.
The motivation is so that we check the wallet file itself as part of the db type checking later on. IIRC there were some issues with where the wrong file type was being used that necessitated these changes.
I’ll try to expand the commit message
in
configure.ac:1180
in
31243f5482outdated
1137@@ -1138,6 +1138,9 @@ fi
1138 if test x$enable_wallet != xno; then
1139 dnl Check for libdb_cxx only if wallet enabled
1140 BITCOIN_FIND_BDB48
1141+
1142+ dnl Check for sqlite3
1143+ AC_CHECK_HEADERS([sqlite3.h], [AC_CHECK_LIB([sqlite3], [sqlite3_open], [SQLITE_LIBS=-lsqlite3], [AC_MSG_ERROR(sqlite3_open missing from libsqlite3)], [-pthread -lpthread])], [AC_MSG_ERROR(sqlite3.h headers missing)])
In commit “Add libsqlite3” (31243f5482bb1c8a71affbe7ced6653a09bd6829)
Would suggest splitting this commit and other build and depends and travis related commits into a separate build PR so it can get feedback from bitcoin build aficionados (and so this PR can more approachable for regular and wallet reviewers).
I think probably build reviewers will want a --with-sqlite configure option to allow sqlite to be disabled in the build even if it is present in the system. They might also want the sqlite location to be determined through pkgconfig instead of assumed to be in the system include directory.
ryanofsky
commented at 10:01 pm on September 30, 2020:
In commit “Add libsqlite3” (10124c3d6c5176d5df94964e530a4b0c6edd8381)
Minor: Maybe someone more familiar with the build can weigh in, but it seems like it could be desirable to allow building bitcoin without sqlite even if sqlite is installed on the system. Also, I think other dependency checks have been switched to use pkg-config (#18307) instead ad-hoc methods like this.
I attempted to make it so that BDB or sqlite could be disabled, but I wasn’t able to get it to work. I’ll leave that for a followup for someone more familiar with autotools.
in
src/wallet/sqlite.h:96
in
08847ca4deoutdated
91@@ -83,6 +92,8 @@ class SQLiteDatabase : public WalletDatabase
9293 /** Make a SQLiteBatch connected to this database */
94 std::unique_ptr<DatabaseBatch> MakeBatch(const char* mode = "r+", bool flush_on_close = true) override;
95+
96+ sqlite3* m_db;
In commit “Constructors, destructors, and relevant private fields for SQLiteDatabase/Batch” (08847ca4de58c112da4b6455230f7b454afa543c)
Minor: Suggest sqlite3* m_db{nullptr}; or sqlite3* m_db = nullptr; here to be sure this is safe without looking and even if someone adds another constructor.
In commit “Introduce g_file_paths” (d038d04a4e3e0522a51cf0d39110749f8395f61a)
Minor: Pretty sure we can get rid of these globals with more sane loading code in the wallet, but in any case could consider switching RecursiveMutex to Mutex if possible and switching cs_sqlite to g_sqlite_mutex to follow newer conventions
ryanofsky
commented at 8:59 pm on July 24, 2020:
member
Started reviewing commit by commit (will update list below with progress), but also have looked over the final diff. Change is surprisingly simple and clean.
If it’s available, I’d be curious to see a diff of the additional changes that switch wallet.dat to wallet.sqlite and how complicated they are. Do we know how current & previous versions of bitcoins react if they load a wallet.dat file that doesn’t contain berkeley db data? Hopefully they show a sensible error instead of crashing obscurely or doing something worse like modifying the file.
b64e40310742e3975ae704a801ccafa73d8617bf wallet: Don’t Verify if database location doesn’t exist (1/31)
6e7ef52e359e55bc82a30207a701995f0ca255d7 Change WalletLocation::Exists to check for wallet file existence (2/31)
Do we know how current & previous versions of bitcoins react if they load a wallet.dat file that doesn’t contain berkeley db data? Hopefully they show a sensible error instead of crashing obscurely or doing something worse like modifying the file.
The wallets won’t be listed by listwalletdir as the bdb magic is checked in that function. BDB itself will error with Not a Berkeley DB (or something like that) when it tries to open the sqlite file and that error makes it’s way to the user.
achow101 force-pushed
on Jul 27, 2020
DrahtBot added the label
Needs rebase
on Jul 27, 2020
achow101 force-pushed
on Jul 27, 2020
DrahtBot removed the label
Needs rebase
on Jul 27, 2020
ryanofsky referenced this in commit
8994145cb3
on Jul 29, 2020
laanwj added this to the "Blockers" column in a project
ryanofsky referenced this in commit
2553091ce5
on Jul 30, 2020
ryanofsky referenced this in commit
0e4cb50e3c
on Jul 30, 2020
in
src/wallet/walletutil.cpp:45
in
e68484878boutdated
52- // Berkeley DB Btree magic bytes, from:
53- // https://github.com/file/file/blob/5824af38469ec1ca9ac3ffd251e7afe9dc11e227/magic/Magdir/database#L74-L75
54- // - big endian systems - 00 05 31 62
55- // - little endian systems - 62 31 05 00
56- return data == 0x00053162 || data == 0x62310500;
57+ return IsBDBFile(path) || IsSQLiteFile(path);
promag
commented at 8:44 pm on August 4, 2020:
member
Concept ACK.
achow101 force-pushed
on Aug 4, 2020
achow101 force-pushed
on Aug 4, 2020
ryanofsky referenced this in commit
9b15bf35d9
on Aug 5, 2020
DrahtBot added the label
Needs rebase
on Aug 5, 2020
achow101 force-pushed
on Aug 5, 2020
DrahtBot removed the label
Needs rebase
on Aug 5, 2020
ryanofsky referenced this in commit
d5c8363b2e
on Aug 7, 2020
ryanofsky referenced this in commit
d503001507
on Aug 7, 2020
in
test/functional/wallet_multiwallet.py:272
in
787047133doutdated
268@@ -269,7 +269,7 @@ def wallet_file(name):
269270 # Fail to load if a directory is specified that doesn't contain a wallet
271 os.mkdir(wallet_dir('empty_wallet_dir'))
272- assert_raises_rpc_error(-18, "Directory empty_wallet_dir does not contain a wallet.dat file", self.nodes[0].loadwallet, 'empty_wallet_dir')
273+ assert_raises_rpc_error(-18, "Wallet empty_wallet_dir not found.", self.nodes[0].loadwallet, 'empty_wallet_dir')
787047133d22f014bea2646d3b23cd18801f19c2: you can drop the else if (fs::is_directory(location.GetPath() branch from rpcwallet.cpp
Sjors
commented at 6:43 pm on August 7, 2020:
member
Reviewed down to b459ada6fd3a5a2ba53cdd132a66ecdf4a574033
I think probably build reviewers will want a –with-sqlite configure option to allow sqlite to be disabled in the build even if it is present in the system.
Conversely, requiring sqlite3 is overkill until descriptor wallets are the default. I suggest disabling descriptor wallets when sqlite3 is either missing or opted out. Later on we’ll need similar code to allow opting out of BDB (disabling legacy wallets).
Would suggest splitting this commit and other build and depends and travis related commits into a separate build PR so it can get feedback from bitcoin build aficionados (and so this PR can more approachable for regular and wallet reviewers).
Worth considering, from my experience with #15382. In that case you can also extract the first two commits and put them in a trivial refactor PR, to make number go down :-)
Depends could also have an opt-out, but no strong feelings there, as long as you can opt-out in the configure phase.
I would also seperate the CI commit from the depends stuff.
Should we constrain the sqlite3 minimum version, perhaps based on their CVE list?
in
src/wallet/sqlite.cpp:22
in
fe790158c8outdated
15+#include <unordered_set>
16+
17+namespace {
18+ Mutex g_sqlite_mutex;
19+ //! Set of wallet file paths in use
20+ std::unordered_set<std::string> g_file_paths GUARDED_BY(g_sqlite_mutex);
ryanofsky
commented at 10:02 pm on August 7, 2020:
In commit “Add/Remove m_file_path to/from g_file_paths in con/destructor” (316002da052cad68c54a48e918d1d5f2ca58c0d0)
Minor: Developer notes and current clang-format config don’t indent namespace contents
Also, I wonder if these variables are even necessary. If sqlite supports opening databases in an exclusive mode, there should be no need for our code to maintain this additional list of database files.
achow101
commented at 11:24 pm on August 10, 2020:
sqlite does have a way to open databases in an exclusive mode but we don’t use it. But we also do that ourselves with the .walletlock file. So I think this is just unnecessary anyways and thus I’ve removed it.
ryanofsky
commented at 1:32 am on August 8, 2020:
member
I took a shot at rebasing this PR on top of #19619:
Change is essentially the same but simpler and I could drop a number of commits. Also changing the database filename from wallet.dat to wallet.sqlite or something else is a one-line change (excluding tests).
achow101 referenced this in commit
39866a9302
on Aug 10, 2020
achow101 force-pushed
on Aug 10, 2020
achow101
commented at 10:11 pm on August 10, 2020:
member
I’ve taken @ryanofsky’s rebase and made a few changes. Notably I removed the filename change and the related tests. Also I made a slight change to CreateWallet` so that the GUI would also make sqlite wallets for descriptor wallets.
achow101 force-pushed
on Aug 10, 2020
achow101 force-pushed
on Aug 11, 2020
achow101 force-pushed
on Aug 12, 2020
DrahtBot added the label
Needs rebase
on Aug 14, 2020
achow101 referenced this in commit
72eaf4c29d
on Aug 14, 2020
achow101 force-pushed
on Aug 14, 2020
ryanofsky referenced this in commit
134f1807e5
on Aug 14, 2020
ryanofsky referenced this in commit
717c4b290f
on Aug 14, 2020
DrahtBot removed the label
Needs rebase
on Aug 14, 2020
ryanofsky referenced this in commit
db66aae6ba
on Aug 17, 2020
ryanofsky referenced this in commit
44f3cf2ba5
on Aug 17, 2020
laanwj removed this from the "Blockers" column in a project
DrahtBot added the label
Needs rebase
on Aug 31, 2020
ryanofsky referenced this in commit
2619e6545e
on Sep 1, 2020
ryanofsky referenced this in commit
a574f21418
on Sep 1, 2020
achow101 force-pushed
on Sep 1, 2020
DrahtBot removed the label
Needs rebase
on Sep 1, 2020
DrahtBot added the label
Needs rebase
on Sep 3, 2020
ryanofsky referenced this in commit
c8e9cd742e
on Sep 3, 2020
ryanofsky referenced this in commit
b5b414151a
on Sep 3, 2020
achow101 force-pushed
on Sep 4, 2020
DrahtBot removed the label
Needs rebase
on Sep 4, 2020
meshcollider referenced this in commit
56d47e19ed
on Sep 6, 2020
meshcollider
commented at 11:48 pm on September 6, 2020:
contributor
achow101
commented at 11:55 pm on September 6, 2020:
member
Rebased
pstratem
commented at 5:18 pm on September 7, 2020:
contributor
Should probably be setting the application_id pragma to something constant and random.
It’s also important that fullfsync be set because Mac OS X is a liar.
Could also be setting user_version, which is a way of versioning the schema.
achow101
commented at 10:05 pm on September 7, 2020:
member
Should probably be setting the application_id pragma to something constant and random.
Could just set it to the network magic bytes? I think that might even let us deal with #12805 by ensuring that we only open a wallet that was created with the correct network magic as the application_id.
Could also be setting user_version, which is a way of versioning the schema.
Would it be useful to set it to the wallet version number or should this just be a new schema version number?
sidhujag referenced this in commit
53b6e698d1
on Sep 9, 2020
achow101
commented at 0:23 am on September 10, 2020:
member
Added application_id as the network magic. Also added user_version.
achow101 force-pushed
on Sep 10, 2020
laanwj added this to the "Blockers" column in a project
DrahtBot added the label
Needs rebase
on Sep 15, 2020
achow101 force-pushed
on Sep 15, 2020
DrahtBot removed the label
Needs rebase
on Sep 15, 2020
in
src/wallet/sqlite.h:106
in
69f7321f8doutdated
101+ /** Make a SQLiteBatch connected to this database */
102+ std::unique_ptr<DatabaseBatch> MakeBatch(const char* mode = "r+", bool flush_on_close = true) override;
103+
104+ sqlite3* m_db{nullptr};
105+
106+ sqlite3_stmt* m_read_stmt = nullptr;
I suppose the locking is done in the calling code. Do I understand correctly that it’s managed by cs_wallet?
nit: I believe it’s better to have all members private. Why not make SQLiteBatch a friend class or just pass the statement handlers when we construct SQLiteBatch object.
achow101
commented at 11:14 pm on September 21, 2020:
I suppose the locking is done in the calling code. Do I understand correctly that it’s managed by cs_wallet?
For the most part, yes. Sometimes we do need to handle concurrency withing SQLiteDatabase but those should already be handled by m_refcount and sqlite itself.
nit: I believe it’s better to have all members private. Why not make SQLiteBatch a friend class or just pass the statement handlers when we construct SQLiteBatch object.
I suppose now we could pass them in. In a previous revision, I don’t think that was possible.
But it’s easier to just let them be public members. It would be 6 extra arguments to pass them in and we are already giving the SQLiteDatabase container.
For the most part, yes. Sometimes we do need to handle concurrency withing SQLiteDatabase but those should already be handled by m_refcount and sqlite itself.
Could you please elaborate on this part? It looks like there are indeed places when we access WalletDatabase instance from another thread without acquiring lock first. For example CWallet::chainStateFlushed. From my understanding of sqlite it’s safe to use by multiple threads, but they should be using different connections. Which is not the case in this example. And I’m not sure how m_refcount will help with that.
I guess in BDB this was handled by cs_db within BerkeleyDatabase::Open
achow101
commented at 4:57 pm on September 22, 2020:
According to https://sqlite.org/threadsafe.html, the default multithreading mode is serialized which means that a single database connection can be used from multiple threads safely. So no locking is needed with that.
I suppose we should enforce that when opening by setting SQLITE_OPEN_FULLMUTEX?
I suppose we should enforce that when opening by setting SQLITE_OPEN_FULLMUTEX?
I think that’s a great idea. I would say that SQLITE_CONFIG_SERIALIZED is even better. It explicitly say that it’s safe to use both connection and prepared statement objects.
achow101
commented at 4:17 pm on September 23, 2020:
DOne
in
src/wallet/sqlite.cpp:142
in
69f7321f8doutdated
121+ return false;
122+ }
123+
124+ // Check the application ID matches our network magic
125+ sqlite3_stmt* app_id_stmt;
126+ ret = sqlite3_prepare_v2(db, "PRAGMA application_id", -1, &app_id_stmt, nullptr);
Actually, I couldn’t find a single place when we create read-only batch at all. Maybe I’m missing something. Do you know what was the original purpose to add read-only mode?
achow101
commented at 4:53 pm on September 22, 2020:
It’s pretty much just a leftover from BDB. The single place a readonly batch is used is in BerkeleyDatabase::Rewrite. Maybe we should just remove the readonly stuff, but that could be done in a followup.
in
src/wallet/sqlite.cpp:212
in
69f7321f8doutdated
226+
227+ if (m_db == nullptr) {
228+ sqlite3* db = nullptr;
229+ int ret = sqlite3_open_v2(m_file_path.c_str(), &db, flags, nullptr);
230+ if (ret != SQLITE_OK) {
231+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database: %s\n", sqlite3_errstr(ret)));
In commit “Implement SQLiteDatabase::Rewrite” (0d75b4013400b5a6bfa1f5c75082a8ff0701bd84)
I think it’s fine
This whole loop doesn’t seem like a good idea. If it actually serves a purpose, there should be a comment about it with specifics. Otherwise I think Rewrite should directly vacuum, and this sleepy loop thing just looks like voodoo.
S3RK
commented at 7:45 am on September 20, 2020:
member
Reviewed all the code. I don’t understand the build system much, but the rest looks solid to me. The only concern that I have is regarding the behaviour when we use multiple batches over one shared connection. Besides that, there are just a few small nits.
I’m going to build the code and test it in upcoming days.
S3RK
commented at 1:54 pm on September 24, 2020:
member
ACK46db9cd
Code reviewed and lightly tested. I built it on macOS 10.13.6 with sqlite (3.19.3) installed from brew.
Created, loaded and unloaded a wallet; loaded multiple sqlite wallets
Verified checks for application_id and schema version (user_version)
Verified SQLite global log configuration; errors appear in the log file as expected
Integrity check gives a proper error while loading wallet from a corrupted file
Wallet encryption; this involves transactions, so I verified it works and no records are visible before commit
Backup
Opened wallet file with another tool and explored the contents of the database
Triggered wallet rewrite and vacuum, verified db size shrinks as expected
fjahr
commented at 2:00 pm on September 25, 2020:
member
Concept ACK and Approach ACK. From my initial shallow pass, the code looks very good already. Local build, automated tests, and some manual testing were all successful. Will keep going and dig into SQLite documentation at the same time since I am not experienced with it.
jonatack
commented at 7:25 pm on September 25, 2020:
member
Concept ACK. On my review shortlist.
fjahr
commented at 6:24 pm on September 26, 2020:
member
I have run coverage checks on the current state of this PR here (these are the total coverage number, i.e. including functional tests). The wallet/sqlite.cpp numbers are low because of many lines dedicated to error checking but also HasKey() and TxnAbort() are not covered while they are covered on the BDB class. I suppose the reason for that is that these functions are not covered by wallet tests but other tests where a BDB based wallet is used. We currently only run the wallet tests with a descriptor wallet AFAICT. Should this be changed now? Maybe there are already plans for this were I missed the discussion.
achow101
commented at 7:50 pm on September 26, 2020:
member
TxnAbort isn’t covered by either SQLite or BDB, and that is expected as it is only called in an failure case.
HasKey seems to be only used by DatabaseBatch::Exists which is currently only used by BerkeleyBatch for a backwards compatibility case that SQLite doesn’t need.
We currently only run the wallet tests with a descriptor wallet AFAICT. Should this be changed now? Maybe there are already plans for this were I missed the discussion.
With #18788, more tests will be enabled for descriptor wallets as well as the option to run all tests with descriptor wallets.
in
ci/test/00_setup_env_native_msan.sh:18
in
5ece9f577aoutdated
My question was unclear: I meant to ask why are we adding SQLite to this environment while we are excluding BDB here?
achow101
commented at 5:54 pm on September 28, 2020:
This test environment is a little weird.
The wallet is actually enabled and BDB is being used. However the BDB being used needs to be built with some special flags, so it can’t be built via depends. What this option does is disable BDB in depends so that BDB can be built separately with whatever it needs to work in this env. Because SQLite doesn’t need this special building, it can be built by depends. So we don’t want NO_WALLET because we actually still want the wallet here. We just want NO_BDB so we can deal with it later.
in
src/wallet/sqlite.cpp:27
in
8c7832a45foutdated
14@@ -15,16 +15,42 @@
1516 static const char* DATABASE_FILENAME = "wallet.dat";
1718+std::atomic<int> g_dbs_open{0};
19+
20+static void ErrorLogCallback(void* arg, int code, const char* msg)
21+{
22+ assert(arg == nullptr); // That's what we tell it to do during the setup
It feels strange to me that the log callback makes everything blow up in a special case. I think we use asserts for developer errors but I don’t think that’s the case here. Also doesn’t this mean that the throw std::runtime_error on L36 will never be used?
achow101
commented at 1:59 am on September 28, 2020:
It feels strange to me that the log callback makes everything blow up in a special case. I think we use asserts for developer errors but I don’t think that’s the case here.
This would only be hit in a developer error or a sqlite bug.
Also doesn’t this mean that the throw std::runtime_error on L36 will never be used?
Hm, the docs aren’t very clear on this (or I can’t find the right place) so I can not say yet if there are cases where ret == SQLITE_OK but the log callback is still being called. That would be the only case when this seems valuable because otherwise the runtime_error would be hit in case there is a problem and the error message would be clearer I think? Yes, this could be a bug in SQLite but in general, we are trusting that the ret/res codes are not lying so I don’t know why we would do it differently here.
On the flip side, the runtime_error will currently only be hit if ret != SQLITE_OK but the ErrorLogCallback is not being called. Again, I couldn’t find details/guarantees on this in the docs but at least they say the callback will be used ‘whenever anomalies occur’, so my expectation is that it would always be called as whenever ret != SQLITE_OK is true. So I think the runtime_error may never be hit unless there is a bug in SQLite.
Overall it just feels strange to do anything but logging in a function that is called *LogCallback.
achow101
commented at 5:59 pm on September 28, 2020:
I think if ret != SQLITE_OK both ErrorLogCallback and runtime_error get called. The ErrorLogCallback doesn’t change ret to SQLITE_OK and it doesn’t throw it’s own exception. The assertion should never fail so that won’t cause a program exit either.
ryanofsky
commented at 5:56 pm on October 1, 2020:
Agree with achow that this is an appropriate place to assert. We’re passing along a context value, and asserting we are passed back the same value. We’re not checking for a runtime error, just documenting an assumption about how the code should is supposed to work, and adding a sanity check to detect if the assumption is wrong.
Perhaps citing from the sqlite3 manual in the comment is more clear:
0// From sqlite3_config() documentation for the SQLITE_CONFIG_LOG option:
1// "The void pointer that is the second argument to SQLITE_CONFIG_LOG is passed through as
2// the first parameter to the application-defined logger function whenever that function is
3// invoked."
4// Assert that this is the case:
promag
commented at 8:21 pm on September 27, 2020:
01b3cb026806784ae20eb670bd9aa40c43a94e54
nit, use initializer {nullptr}?
achow101
commented at 4:24 pm on September 30, 2020:
Done
in
src/wallet/walletdb.cpp:1049
in
94d5ee3067outdated
1042+ error = Untranslated(strprintf("Failed to load database path '%s'. Data is not in required format.", path.string()));
1043+ status = DatabaseStatus::FAILED_BAD_FORMAT;
1044+ return nullptr;
1045+ }
1046+
1047+ if (!format && options.require_format) format = options.require_format;
I am still not an SQLite expert but at least I have sanity checked all the statements and dug a bit deeper on a few of them. I left a few comments but I don’t consider any of them blocking. Will do some further manual testing as well soon.
There are no changes to the docs so far. Do you plan to do these in a follow-up? It might be good to work on this in parallel or add the most important bits here since when this gets merged some people might run into build issues while the doc changes are being bike-shedded.
achow101
commented at 1:59 am on September 28, 2020:
member
It’s not clear to me how concurrent accesses are handled, how sqlite guarantees a concurrent thread B waits until after thread A calls sqlite3_reset.
ryanofsky
commented at 10:11 pm on September 30, 2020:
In commit “Add SQLiteDatabase and SQLiteBatch dummy classes” (f35f89f1ae53914f6183ec3d7da6a1d0c27bed16)
See main comment for reasoning, but this is awkward to change later so I think the commit introducing this should start with the appropriate final value (hopefully “wallet.sqlite” or similar)
ryanofsky
commented at 10:21 pm on September 30, 2020:
In commit “Initialize and Shutdown sqlite3 globals” (13cca0d881acc81dbf4a6496bb02d4fefc905a77)
There’s still a race condition here if two databases are opened at the same time. sqlite3_config will only be called once for both databases, but only one of the databases will actually block waiting for the call to complete, so the other database open will most likely fail. Similar races can happen on close.
Instead of trying to be clever and lock-free, it would be better to be dumb and mutex-full.
ryanofsky
commented at 6:22 pm on October 1, 2020:
In commit “Implement SQLiteDatabase::Open” (ecc06767e1a91b11179ffb5b70808b69a9c6b552)
Note for future improvement: We should probaby stop this non-standard use of flags. “w” or “a” normally creates a new file not “c”. https://www.gnu.org/software/libc/manual/html_node/Opening-Streams.html. It might also be better to switch away from modes strings to readonly / require_new / require_existing boolean options.
I agree that flags would probably be better. Also, I think there’s an argument for dropping read_only considering it is only used by BerkeleyDatagbase::Rewrite.
I did an implementation based on this PR to move from modes string to bool flags; and removed read-only mode as well. I’ll open a PR once this one will be merged.
ryanofsky
commented at 1:09 pm on October 6, 2020:
I did an implementation based on this PR to move from modes string to bool flags; and removed read-only mode as well. I’ll open a PR once this one will be merged.
Since these changes would only simplify this PR, and the more complicated parts don’t involve sqlite, I think you could open a separate PR based on master just omitting the sqlite parts. Also, the change can go further and drop the batch create option, just using the DatabaseOptions::require_existing option instead (storing it if needed).
in
src/wallet/sqlite.cpp:86
in
ecc06767e1outdated
82+
83+ if (create) {
84+ bool table_exists;
85+ // Check that the main table exists
86+ sqlite3_stmt* check_main_stmt;
87+ std::string check_main = "SELECT name FROM sqlite_master WHERE type='table' AND name='main'";
ryanofsky
commented at 6:27 pm on October 1, 2020:
In commit “Implement SQLiteDatabase::Open” (ecc06767e1a91b11179ffb5b70808b69a9c6b552)
ryanofsky
commented at 6:32 pm on October 1, 2020:
In commit “Implement SQLiteDatabase::Open” (ecc06767e1a91b11179ffb5b70808b69a9c6b552)
I think you need to do this check even in the (m_db == nullptr) case above in case the file or filesystem is read-only. According to https://www.oreilly.com/library/view/using-sqlite/9781449394592/re303.html SQLITE_OPEN_READWRITE will “Attempt to open the file read/write. If this is not possible, open the file read-only. Opening the file read-only will not result in an error.”
42@@ -42,6 +43,18 @@ SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_pa
43 }
44 }
4546+bool SQLiteDatabase::PrepareDirectory() const
47+{
48+ if (m_mock) return true;
49+ // Try to create the directory containing the wallet file and lock it
50+ TryCreateDirectories(m_dir_path);
51+ if (!LockDirectory(m_dir_path, ".walletlock")) {
ryanofsky
commented at 7:02 pm on October 1, 2020:
In commit “sqlitedb: Create and lock the wallet directory” (af798a8fd9b3b1a1e0a63a665f6d89c73e8614a3)
This doesn’t seem like the most ideal approach. We were forced to use .walletlock in #11904 because the old version of BDB we use doesn’t support set_lk_exclusive. But with sqlite it would seem simpler and safer to use its exclusive locking functionality: https://www.sqlite.org/pragma.html#pragma_locking_mode instead of rolling our own which is probably less portable and reliable, requires code duplication, and is nonstandard not working with other sqlite database tools.
Since only an EXCLUSIVE lock is acquired after the first write, I’m not sure that the locking pragma provides strong enough guarantees for us. Another bitcoind could conceivably open the same wallet while only the SHARED lock is held.
ryanofsky
commented at 2:37 am on October 6, 2020:
In commit “sqlitedb: Create and lock the wallet directory” (4f3e5569c3c0eb83d50f7f88e9b673fa857fa08a)
Since only an EXCLUSIVE lock is acquired after the first write, I’m not sure that the locking pragma provides strong enough guarantees for us. Another bitcoind could conceivably open the same wallet while only the SHARED lock is held.
No bitcoind should open the wallet in shared mode. The simplest approach is for every bitcoind to open the wallet in exclusive mode and fail if exclusive lock cannot be acquired
I did write another suggestion about opening the database earlier to simplify code and avoid closing, unlocking, and reopening the database after verifying, but this is orthogonal to type of locking used.
The first time the database is read in EXCLUSIVE mode, a shared lock is obtained and held. The first time the database is written, an exclusive lock is obtained and held.
So opening in exclusive mode means that there is a potential case where bitcoind has not yet written anything and thus only has a shared lock. In that case, another bitcoind could open the same file (and not write anything) and also acquire a shared lock. See https://sqlite.org/lockingv3.html for the definitions of shared and exclusive locks in sqlite3.
ryanofsky
commented at 3:24 am on October 7, 2020:
In commit “sqlitedb: Create and lock the wallet directory” (a6415a4dc752db171bd842fb376c5c5a919c04e5)
Oh, you are absolutely right. Just setting the pragma doesn’t get the lock, you have to do an empty transaction to acquire it. This just means you need 3 statements to open the database: pragma locking_mode=exclusive; begin exclusive transaction; commit; instead of 1 statement.
Using the sqlite exclusive lock would be more reliable than .walletlock and simpler because it would be a part of database opening, not in some other code path separate from opening, and because it would no longer requiring extra code removing .walletlock at shutdown (this code already had a bug previously in this PR).
In case it helps, I found it pretty straightforward to test locking from the command line:
This just means you need 3 statements to open the database:
Then there’s the potential for race conditions in 2 instances: before the pragma is executed, and after it is executed but before the transaction begins.
Edit: Oh I guess we just exit with the database is in use error when begin transaction fails.
ryanofsky
commented at 4:49 pm on October 7, 2020:
Then there’s the potential for race conditions in 2 instances: before the pragma is executed, and after it is executed but before the transaction begins.
There’s no race condition here:
0db = sqlite_open(path); // Or error "sorry, couldn't open database"
1sqlite_exec(db, "pragma locking_mode=exclusive"); // Or error "sorry, couldn't set locking mode"
2sqlite_exec(db, "begin exclusive transaction"); // Or error "sorry, database is currently in use"
3sqlite_exec(db, "commit"); // Or error "sorry, unexpected failure"
There would be a race condition if the exclusive lock was released after the commit statement. But the point of the pragma is to extend the exclusive lock from point where it is first acquired until the point where the database is closed.
c76a25c125214cf69ddcd5493c9457195a9d768f Include sqlite3 in documentation (27/27)
Main two pieces of feedback are:
I think this change should be merged with more tests enabled. Particularly wallet_backup.py and wallet_multiwallet.py tests would be good to have running, but probably others would be good as well. Doing this is pretty straightforward after #20034, and I took a shot at in a branch: https://github.com/bitcoin/bitcoin/compare/master...ryanofsky:pr/sql (branch)
I think this change should start off on the right foot using sqlite naming and locking conventions. We don’t disguise sqlite journal or WAL log files as BDB log files, and I don’t think there’s a reason to disguise the new data file with the old name. Using “wallet.sqlite” clearly identifies file format and wallet directory type to provide transparency and help debugging, and should avoid tools that aren’t equipped to access these files from trying to access them
achow101
commented at 7:54 pm on October 1, 2020:
member
I think this change should be merged with more tests enabled. Particularly wallet_backup.py and wallet_multiwallet.py tests would be good to have running, but probably others would be good as well. Doing this is pretty straightforward after #20034, and I took a shot at in a branch: master…ryanofsky:pr/sql (branch)
I think this change should start off on the right foot using sqlite naming and locking conventions. We don’t disguise sqlite journal or WAL log files as BDB log files, and I don’t think there’s a reason to disguise the new data file with the old name. Using “wallet.sqlite” clearly identifies file format and wallet directory type to provide transparency and help debugging, and should avoid tools that aren’t equipped to access these files from trying to access them
I have two main arguments to retain the wallet.dat naming:
Existing external backup tooling, documentation, and other information refer to the wallet file as wallet.dat. In the vast majority of those cases, the exact file format is irrelevant so they will still be applicable to a sqlite wallet.dat. In the cases where the file format matters (only pywallet AFAIK), the file format is checked so the tool will error if it isn’t correct.
If the user has specified a sqlite wallet in their conf file, downgrading may result in a new bdb wallet.dat file being created in that directory. This can be confusing the user (they “lost” all of their money) and potentially issues with a subsequent upgrade to a version with sqlite (there would be both wallet.dat and wallet.sqlite in the same dir). I think it would be better to explicitly error (and a sqlite wallet.dat would cause an error for old software) rather than fail silently.
achow101 force-pushed
on Oct 1, 2020
in
depends/packages/sqlite.mk:2
in
61b650b6f7outdated
in
src/wallet/sqlite.cpp:254
in
0344ccea4doutdated
84+ }
85+ // TODO: Maybe(?) Check the file wasn't copied and a duplicate opened
86+
87+ if (create) {
88+ // Make the table for our key-value pairs
89+ std::string create_stmt = "CREATE TABLE IF NOT EXISTS main(key BLOB PRIMARY KEY, value BLOB)";
0344ccea4dcc0692e736dd759fc3945272a3266c let’s drop IF NOT EXISTS. If create is only set for new database then no table should exist. Alternatively, if create is meant as “create if needed”, then it’s seems safer to explicitly check if the table doesn’t exist yet. In that case the error “Failed to create new database” is incorrect.
I think it’s safer to explicitly check if the table is present. In general it’s nice to have a clearly separate code path for stuff we only do on wallet creation (and upgrade).
ryanofsky
commented at 2:54 pm on October 5, 2020:
I think it’s safer to explicitly check if the table is present. In general it’s nice to have a clearly separate code path for stuff we only do on wallet creation (and upgrade).
Dropping ‘if not exists’ and adding an error sounds good (though it might be helpful to say what is unsafe or what dangerous cases you are thinking of). I only suggested using ‘if not exists’ SQL syntax to avoid reimplementing it in C++.
Alternatively, if create is meant as “create if needed”, then it’s seems safer to explicitly check if the table doesn’t exist yet. In that case the error “Failed to create new database” is incorrect.
It is use as a “create if needed.” All wallet DBs are created with the mode “cr+”. I don’t think we should be giving an error if the table doesn’t exist in that case either.
ryanofsky
commented at 1:58 am on October 6, 2020:
It is use as a “create if needed.” All wallet DBs are created with the mode “cr+”. I don’t think we should be giving an error if the table doesn’t exist in that case either.
It would require changes outside sqlite.cpp/sqlite.h but it should be possible to throw an error instead of defensively creating tables if the database already already exists and the tables are missing. I think it would be a little safer and probably preferable, but not critical.
in
src/wallet/sqlite.cpp:266
in
0344ccea4doutdated
90+ ret = sqlite3_exec(db, create_stmt.c_str(), nullptr, nullptr, nullptr);
91+ if (ret != SQLITE_OK) {
92+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to create new database: %s\n", sqlite3_errstr(ret)));
93+ }
94+
95+ // Enable fullfysnc for the platforms that use it
0344cce: is PRAGMA fullfsync ephemeral? Documentation isn’t very clear about that. If so, it shouldn’t be under create. On macOS 10.15.7 with sqlite 3.28.0 when I call PRAGMA fullfsync; it returns 0.
Update: it’s fine, create is confusingly named, but this is code is run every time you load a wallet
0344cce: you may want to add this check early in the create block too. E.g. if I make an existing wallet read-only, it will throw a confusing “Failed to set the application id” (its first attempt to write).
Sjors
commented at 12:11 pm on October 5, 2020:
member
Reviewed the first couple of commits (to bb2406c9029ad66833f9d2fc62335872204ee94c)
Lightly tested by creating a Signet descriptor wallet and sending some coins back and forth.
macOS 10.15.7 seems to come with sqlite3 version 3.28.0 bundled. When installing sqlite3 via homebrew, you’ll get the latest 3.33.0 but it’s keg-only. This causes pkg-config to pick up version 3.28, unless you do:
98@@ -99,6 +99,10 @@ The following can be set when running make: `make FOO=bar`
99 <dd>Don't download/build/cache packages needed for enabling zeromq</dd>
100 <dt>NO_WALLET</dt>
101 <dd>Don't download/build/cache libs needed to enable the wallet</dd>
102+<dt>NO_BDB</dt>
103+<dd>Don't download/build/cache BerkeleyDB</dd>
104+<dt>NO_SQLITET</dt>
ryanofsky
commented at 11:51 pm on October 5, 2020:
In commit “Add sqlite to travis and depends” (8bfbad00d4bd94f2b9006e6375ea7dc11adee6fb)
60+ void Open(const char* mode) override;
61+
62+ /** Close the database */
63+ void Close() override;
64+
65+ /** Indicate the a new database user has began using the database. Increments m_refcount */
ryanofsky
commented at 0:18 am on October 6, 2020:
In commit “Add SQLiteDatabase and SQLiteBatch dummy classes” (b6eeb2b5ee6964a9034aca6f4be2b72d532589ca)
I think the refcounting code in this PR is confusing and should be removed. Rewrite method can just vacuum without sleeping. Close method can unconditionally close m_db it’s not null. m_refcount doesn’t ever need be referenced, AddRef and RemoveRef don’t ever need to be called, and they be implemented in a transparent way to make it clear they are unused holdovers:
ryanofsky
commented at 0:42 am on October 6, 2020:
In commit “Implement SQLiteBatch::Close” (fbf44184de758271e759978cd5037efcb4ba4eb1)
This should log an error if TxnAbort() fails. It might also be useful to log an error if TxnAbort succeeds, since this condition should never happen unless there is unpaired TxnBegin call or an unhandled exception.
ryanofsky
commented at 1:50 am on October 6, 2020:
In commit “Constructors, destructors, and relevant private fields for SQLiteDatabase/Batch” (46aa20b62e77b33e62e1e17ac6f6a6eb1a9efdce)
I don’t think it makes sense to open the database in the batch constructor instead of in the database constructor. It makes the batch constructor assymetric (there’s no database close in the destructor), forces database to be needlessly opened, closed, and reopened when verification is enabled, delays error checking done on open (making debugging and diagnosis more difficult), and prevents taking full advantages of sqlite’s exclusive locking to prevent the database from being opened in different processes.
The purpose of opening the database within a batch is to only open it when the database is going to be used. It is then left open to avoid constantly closing and reopening it.
We also do it to set the mode on the database, but the usefulness of that is arguable but we can deal with that in a followup.
ryanofsky
commented at 1:43 am on October 7, 2020:
The purpose of opening the database within a batch is to only open it when the database is going to be used. It is then left open to avoid constantly closing and reopening it.
It sounds like my suggestion was not clear. My suggestion is to open the database when the database object is created, and close it when it is destroyed. This opens and closes the database fewer times than the current implementation (due to not having to open close and reopen for verification) and is simpler (also due to not having to open close and reopen for verification). Feel free to ignore the suggestion or to follow it up later, but I hope I am getting it across clearly.
in
src/wallet/sqlite.cpp:89
in
96d2a5f8b8outdated
85+ sqlite3* db = nullptr;
86+ int ret = sqlite3_open_v2(m_file_path.c_str(), &db, flags, nullptr);
87+ if (ret != SQLITE_OK) {
88+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database: %s\n", sqlite3_errstr(ret)));
89+ }
90+ // TODO: Maybe(?) Check the file wasn't copied and a duplicate opened
ryanofsky
commented at 2:14 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Open” (96d2a5f8b8ea7dd512deaac4d1d70702517af6c1)
Should drop this comment. Copying a wallet may be a problem if you do it in a crazy way, or it may be perfectly safe. BDB code started checking for copies only as a kludge (initial kludge protected against ambiguity in the bdb log format when multiple databases were opened in the same environment, and later the kludge was extended to work around inability to lock BDB databases with set_lk_exclusive.)
93+ if (sqlite3_db_readonly(db, "main") != 0) {
94+ throw std::runtime_error("SQLiteDatabase: Database opened in readonly mode but read-write permissions are needed");
95+ }
96+
97+ // Make the table for our key-value pairs
98+ std::string create_stmt = "CREATE TABLE IF NOT EXISTS main(key BLOB PRIMARY KEY, value BLOB)";
ryanofsky
commented at 2:17 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Open” (96d2a5f8b8ea7dd512deaac4d1d70702517af6c1)
Should add NOT NULL constraint to key and value columns. Code already has to deal with empty string values, so it would be better not to throw in NULL values and errors as well where we don’t need them.
in
src/wallet/sqlite.cpp:132
in
fabd187ccboutdated
116+ if (!PrepareDirectory()) return false;
117+
118+ sqlite3* db = nullptr;
119+ int ret = sqlite3_open_v2(m_file_path.c_str(), &db, SQLITE_OPEN_READONLY, nullptr);
120+ if (ret == SQLITE_NOTFOUND) {
121+ sqlite3_close(db);
ryanofsky
commented at 2:50 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Verify” (fabd187ccb43d0cc266ae89cd2b2e3b3d098949a)
I don’t think you can close the db if opening failed. I think even on cases where we don’t act on errors, we should at least log them. It would be best if all the sqlite_* calls in this PR logged errors so we are never blindly debugging or guessing about errors like this.
Whether or not an error occurs when it is opened, resources associated with the database connection handle should be released by passing it to sqlite3_close() when it is no longer required.
Thanks for explaining sqlite3_close. How do you know whether or not something is logged to the error log? Does every function call that doesn’t return SQLITE_OK log to the error log? If so, then I guess there’s no technical reason to handle errors twice even if code looks like it is silently discarding them. I would prefer the uniform approach of handling all errors, but this would give more of excuse not to.
This is my current understanding of how it works based on my tests and the following line from documentation.
This looks like a misreading to me. The documentation could easily say every failing call is logged, but it doesn’t, and even goes out of it’s way to say it “strives to keep error logger traffic low.” But even all errors were guaranteed to be logged, would you expect someone else reading the code to know it? Or will the next person maintaining or debugging this code waste development time checking unchecked return values, instead of just being able to look at the code and see that every call is succeeding. That is why if I were writing this code, I would check for unexpected return values everywhere and print them. Fixing this is just a suggestion. It is fine to ignore the suggestion.
Indeed I assumed (maybe falsely) that the next maintainer is familiar with SQLite C api. My humble personal preference is not to complicate the code by duplicating the error messages, when the library can do it for us. I just tried to provide more context for the author and the reviewers, sorry if it wasn’t helpful.
ryanofsky
commented at 2:12 pm on October 6, 2020:
Indeed I assumed (maybe falsely) that the next maintainer is familiar with SQLite C api. My humble personal preference is not to complicate the code by duplicating the error messages, when the library can do it for us. I just tried to provide more context for the author and the reviewers, sorry if it wasn’t helpful.
This was helpful, but it’s not just assuming some basic level of familiarity with sqlite, it is assuming knowledge of undocumented behavior of the API. Debug logging and error handling are two overlapping things code can do, but neither is a subset of the other. We aren’t using bash/go/C style programming, and we have good non-verbose error handling options available, so I wouldn’t ignore error codes just because sqlite has a debug feature that prints some subset of errors with less context.
Whether or not an error occurs when it is opened, resources associated with the database connection handle should be released by passing it to sqlite3_close() when it is no longer required.
Since it is ok for a database to not exist when we do Verify, I think it is correct to not log anything on the NOTFOUND error here.
in
src/wallet/sqlite.cpp:170
in
fabd187ccboutdated
ryanofsky
commented at 2:52 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Verify” (fabd187ccb43d0cc266ae89cd2b2e3b3d098949a)
This looks like a bug. You can’t delete a prepared statement that wasn’t created, and this might lead to a bad pointer dereference since since stmt pointer is not initialized.
As an out parameter It’s set to NULL in case of an error. This is documented and to me the code in question looks like an intended use of sqlite api. The sqlite3_finalize routine can be called at any point during the life cycle. I’m not sure whether it’s possible for an error to happen when statement is initialized. But it looks reasonable and safer to finalize whenever we encounter an error at any step of the life cycle.
ryanofsky
commented at 11:43 am on October 6, 2020:
As an out parameter It’s set to NULL in case of an error. This is documented and to me the code in question looks like an intended use of sqlite api.
Sorry, where is this documented? Even assuming it is documented, would you expect anybody else reading the code to know this? Or will the next person maintaining or debugging this waste development time checking it, instead of just being able to look and see that the code won’t dereference an uninitialized pointer. If I were writing this, I would initialize the pointer to null. Fixing this is just a suggestion. It is fine to ignore the suggestion.
Just not to sound unfounded — here is the link https://www.sqlite.org/c3ref/prepare.html
Maybe I misunderstood your point, I didn’t mean to say that I’m against initializing it to null, it’s a good idea. It was more to the fact that I believe calling finalize is fine in the case of an error.
ryanofsky
commented at 3:10 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Verify” (fabd187ccb43d0cc266ae89cd2b2e3b3d098949a)
It is odd to lock the wallet here because it means the wallet will be locked on opening when options.verify is true, but not locked until later when options.verify is false (later when the first batch is created).
The simplest thing approach would seem to just open and lock the database once when the database object is created instead of opening and locking multiple times in different circumstances.
ryanofsky
commented at 3:17 am on October 6, 2020:
In commit “Implement SQLiteDatabase::Flush, PeriodicFlush, and ReloadDbEnv as No-ops” (addc4630a2a6587b0e07b9c45644117542685fed)
All the comments in the commit description should be comments in the code, so they are not lost in git history and so code makes sense. Would also suggest implementing these methods inline in the header file to avoid giving a misleading impression from reading the class definition that they are implemented to do things.
ryanofsky
commented at 3:21 am on October 6, 2020:
In commit “sqlitedb: Create and lock the wallet directory” (4f3e5569c3c0eb83d50f7f88e9b673fa857fa08a)
This fails to unlock the wallet if it was opened with options.verify = true, but m_db is null because no batch was created. In practice, I’m guessing this never happens, but this is the type of complication which would go away if we used sqlite locking instead of implementing our own locking.
I’ve removed if (!m_db) return; so this will always be run. This is safe because sqlite3_close on a nullptr is a no-op. This will unconditionally unlock the directory on db close.
in
src/wallet/sqlite.cpp:154
in
e63b950425outdated
in
src/wallet/sqlite.cpp:258
in
21289de5fdoutdated
250@@ -251,22 +251,106 @@ void SQLiteBatch::Close()
251252 bool SQLiteBatch::ReadKey(CDataStream&& key, CDataStream& value)
253 {
254- return false;
255+ if (!m_database.m_db) return false;
256+ assert(m_database.m_read_stmt);
257+
258+ // Bind: leftmost parameter in statement is index 1
259+ int res = sqlite3_bind_blob(m_database.m_read_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
ryanofsky
commented at 3:47 am on October 6, 2020:
In commit “Implement SQLiteBatch::ReadKey, WriteKey, EraseKey, and HasKey” (21289de5fd85b4510032393902b0dec84704cf77)
This whole commit does not seem thread safe. Wallet RPC calls can come in from multiple threads and validation events come in on their own thread, so I would think multiple batch objects and reads and writes could happen simultaneously. If this is the case bind_blob/step calls to the same statement objects from different threads would all interfere with each other.
The simplest approach would seem to just create prepared statements on demand instead of trying to share them between different batches.
You can use the same prepared statement from different threads but you can’t use the same statement for different purposes with different values at the same time from different threads. The only thing that might prevent bugs in practice is the fact that most of the database is copied into memory and not actually accessed through database lookups, and the fact that the cs_wallet mutex makes most wallet operations single threaded in practice. But these things only make the code accidentally correct instead of correct by design. The sqlite implementation shouldn’t have object sharing and reference counting code that makes it more complicated than it needs to be, and less safe than the bdb implementation (or any sane key/value API). You shouldn’t get undefined results, for example, simply reading two keys from two different threads.
Now I think you’re correct. Looks like the locking in sqlite will guarantee safety for a single call, but we ourselves still have to take care for the sequence of the calls.
You can use the same prepared statement from different threads but you can’t use the same statement for different purposes with different values at the same time from different threads.
This is what I failed to articulate before and was to quick to agree it’s safe.
ryanofsky
commented at 3:52 am on October 6, 2020:
In commit “Implement SQLiteDatabase::TxnBegin, TxnCommit, and TxnAbort” (d448c6c362642f24b08b754cb09066ced847f867)
It would seem best to log errors if these calls fail. TxnBegin, TxnCommit, TxnAbort should never return false unless there’s a bug in our code or a runtime error and in either case having more debug information would be useful
4e8011a (maybe for followup): does it make sense to make this a prepared statement?
No. There’s nothing to prepare.
ryanofsky
commented at 4:22 am on October 6, 2020:
member
I’m think I’m like 75% done with this review. I’m posting comments I have so far just because I think one new comment below about prepared statement thread safety is critical and should be addressed before this PR is merged (see “This whole commit does not seem thread safe” below).
Overall, there are a lot of things here I think should be cleaned up and simplified, but almost all of them could be implemented as followups after this is merged. The only three things I think are critical to change before this PR is merged are easy to patch in:
Not sharing prepared statements between threads
Enabling more functional tests for sqlite wallets (particularly multiwallet and wallet backup tests)
Setting data filename to avoid various forward & backward compatibility complications
achow101 force-pushed
on Oct 6, 2020
achow101 force-pushed
on Oct 6, 2020
achow101 force-pushed
on Oct 6, 2020
achow101 force-pushed
on Oct 6, 2020
achow101
commented at 6:39 pm on October 6, 2020:
member
I’ve also made a few changes to pass a couple of tests with #18788 merged. https://github.com/achow101/bitcoin/tree/sqlite-master is a branch with this and #18788 merged in, based on master. That branch also has an additional commit so that tool_wallet works with sqlite.
in
src/wallet/sqlite.h:70
in
f747380289outdated
65+ /** Indicate the a new database user has began using the database. Increments m_refcount */
66+ void AddRef() override;
67+ /** Indicate that database user has stopped using the database. Decrement m_refcount */
68+ void RemoveRef() override;
69+
70+ /** Rewrite the entire database on disk, with the exception of key pszSkip if non-zero
ryanofsky
commented at 1:28 am on October 7, 2020:
In commit “Add SQLiteDatabase and SQLiteBatch dummy classes” (f747380289b06ba1631e1cec30b96b5aecb61f36)
73+
74+ /** Back up the entire database to a file.
75+ */
76+ bool Backup(const std::string& dest) const override;
77+
78+ /** Make sure all changes are flushed to disk.
ryanofsky
commented at 1:33 am on October 7, 2020:
In commit “Add SQLiteDatabase and SQLiteBatch dummy classes” (f747380289b06ba1631e1cec30b96b5aecb61f36)
Comment isn’t true since method is a no-op. I think this method and other methods which are no-ops like PeriodicFlush ReloadDbEn or never called AddRef RemoveRef should be inlined here so it’s obvious what their role is in the sqlite implementation and how they are not normal methods
76+ bool Backup(const std::string& dest) const override;
77+
78+ /** Make sure all changes are flushed to disk.
79+ */
80+ void Flush() override;
81+ /* flush the wallet passively (TRY_LOCK)
ryanofsky
commented at 1:33 am on October 7, 2020:
In commit “Add SQLiteDatabase and SQLiteBatch dummy classes” (f747380289b06ba1631e1cec30b96b5aecb61f36)
Comment is also incorrect
in
src/wallet/sqlite.cpp:92
in
d1559561a6outdated
87+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database: %s\n", sqlite3_errstr(ret)));
88+ }
89+
90+ if (create) {
91+ if (sqlite3_db_readonly(db, "main") != 0) {
92+ throw std::runtime_error("SQLiteDatabase: Database opened in readonly mode but read-write permissions are needed");
ryanofsky
commented at 1:56 am on October 7, 2020:
In commit “Implement SQLiteDatabase::Open” (d1559561a68dd6604efd91baa5dbd28c995d0074)
It looks like db pointer can be leaked here if this error is thrown
I’ve changed this to just use m_db so on failure it will be cleaned up by the destructor.
in
src/wallet/sqlite.cpp:99
in
d1559561a6outdated
94+
95+ // Make the table for our key-value pairs
96+ std::string create_stmt = "CREATE TABLE IF NOT EXISTS main(key BLOB PRIMARY KEY, value BLOB NOT NULL)";
97+ ret = sqlite3_exec(db, create_stmt.c_str(), nullptr, nullptr, nullptr);
98+ if (ret != SQLITE_OK) {
99+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to create new database: %s\n", sqlite3_errstr(ret)));
ryanofsky
commented at 1:57 am on October 7, 2020:
In commit “Implement SQLiteDatabase::Open” (d1559561a68dd6604efd91baa5dbd28c995d0074)
It looks like db pointer can be leaked here if ret is not OK.
in
src/wallet/sqlite.cpp:96
in
d1559561a6outdated
91+ if (sqlite3_db_readonly(db, "main") != 0) {
92+ throw std::runtime_error("SQLiteDatabase: Database opened in readonly mode but read-write permissions are needed");
93+ }
94+
95+ // Make the table for our key-value pairs
96+ std::string create_stmt = "CREATE TABLE IF NOT EXISTS main(key BLOB PRIMARY KEY, value BLOB NOT NULL)";
ryanofsky
commented at 2:21 am on October 7, 2020:
In commit “Implement SQLiteDatabase::Open” (d1559561a68dd6604efd91baa5dbd28c995d0074)
key needs NOT NULL constraint to prevent nulls (primary key isn’t enough).
0$ sqlite3
1SQLite version 3.30.1 2019-10-10 20:19:45
2sqlite> CREATE TABLE IF NOT EXISTS main(key BLOB PRIMARY KEY, value BLOB NOT NULL);
3sqlite> insert into main values (null, 'value');
4sqlite> select * from main;
5|value
in
src/wallet/sqlite.cpp:106
in
d1559561a6outdated
101+
102+ // Enable fullfysnc for the platforms that use it
103+ std::string fullfsync_stmt = "PRAGMA fullfsync = true";
104+ ret = sqlite3_exec(db, fullfsync_stmt.c_str(), nullptr, nullptr, nullptr);
105+ if (ret != SQLITE_OK) {
106+ throw std::runtime_error(strprintf("SQLiteDatabase: Failed to enable fullfsync: %s\n", sqlite3_errstr(ret)));
ryanofsky
commented at 2:22 am on October 7, 2020:
In commit “Implement SQLiteDatabase::Open” (d1559561a68dd6604efd91baa5dbd28c995d0074)
It looks like db pointer can be leaked here if ret is not OK.
in
src/wallet/sqlite.cpp:177
in
29395e2869outdated
171@@ -172,6 +172,13 @@ void SQLiteBatch::Flush()
172173 void SQLiteBatch::Close()
174 {
175+ if (m_database.m_db && sqlite3_get_autocommit(m_database.m_db) == 0) {
176+ if (TxnAbort()) {
177+ LogPrintf("SQLiteBatch: Batch closed and transaction was aborted\n");
ryanofsky
commented at 2:30 am on October 7, 2020:
In commit “Implement SQLiteBatch::Close” (29395e28698978dc65e16c4eaf9bb8f8e7429e7d)
Would be good to s/Batch closed/Batch closed unexpectedly without explicit commit or abort/ here and below to be clear this is an error, not an informational log
in
src/wallet/sqlite.cpp:357
in
f86afc5340outdated
238@@ -202,6 +239,28 @@ void SQLiteBatch::Close()
239 LogPrintf("SQLiteBatch: Batch closed and could not abort transaction\n");
240 }
241 }
242+
243+ // Free all of the prepared statements
244+ int ret = sqlite3_finalize(m_read_stmt);
245+ if (ret != SQLITE_OK) {
ryanofsky
commented at 3:35 am on October 7, 2020:
In commit “Add SetupSQLStatements” (f86afc53406448428dadaf59d0ccf78fb1421220)
It looks like there are double-delete bugs in this method depending on how it’s called. Statement pointers are not set to null if finalize returns SQLITE_OK, so if Close is called twice sqlite3_finalize will be called with invalid deleted pointers and possibly segfault.
One fix would be to set the pointers to null if finalize returns SQLITE_OK. Another fix would be move the code out of this method into the destructor.
in
src/wallet/sqlite.cpp:392
in
36ea015032outdated
270+ assert(m_read_stmt);
271+
272+ // Bind: leftmost parameter in statement is index 1
273+ int res = sqlite3_bind_blob(m_read_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
274+ if (res != SQLITE_OK) {
275+ sqlite3_clear_bindings(m_read_stmt);
ryanofsky
commented at 3:44 am on October 7, 2020:
In commit “Implement SQLiteBatch::ReadKey, WriteKey, EraseKey, and HasKey” (36ea015032ddf1aa7d424b463c2c6c531d832295)
Throughout the PR would be good to check all return codes and log errors with context information whenever any sqlite3_* calls fail. But if the practice of checking return codes is going to be set aside when calling sqlite, feel free to ignore the suggestion.
in
src/wallet/sqlite.cpp:486
in
36ea015032outdated
356+ if (!m_database.m_db) return false;
357+ assert(m_read_stmt);
358+
359+ // Bind: leftmost parameter in statement is index 1
360+ bool ret = false;
361+ int res = sqlite3_bind_blob(m_read_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
ryanofsky
commented at 3:52 am on October 7, 2020:
In commit “Implement SQLiteBatch::ReadKey, WriteKey, EraseKey, and HasKey” (36ea015032ddf1aa7d424b463c2c6c531d832295)
Theoretically it might be more efficient to use
SELECT EXISTS(SELECT * FROM main WHERE key = ?) instead of SELECT value FROM main WHERE key = ? to avoid needing to read the table and just use the index. But probably does not matter in practice.
As a result of this, I’ve also dropped the usage of mode and the database will always be created if it does not exist. This is fine as we enforce existence during MakeDatabase, but it would be better to have a create flag for this.
ryanofsky
commented at 3:38 pm on October 7, 2020:
In commit “Implement SQLiteDatabase::Open” (d1559561a68dd6604efd91baa5dbd28c995d0074)
Looking at S3RK’s branchhttps://github.com/bitcoin/bitcoin/pull/19077/#discussion_r499125987, I don’t see a need to parse the mode argument at all. I think SQLiteDatabase::Open should ignore the mode argument just like SQLiteDatabase::Rewrite ignores the skip argument. It would simplify the implementation here in various ways. And later I think the mode argument should be dropped from BDB as well in favor of simply respecting the DatabaseOptions.require_existing value passed when creating the database object.
ryanofsky changes_requested
ryanofsky
commented at 4:28 pm on October 7, 2020:
member
This is looking great! This is already much simpler and nicer than the BDB implementation, and I’d be ready to ACKd8e024121b54fc998c61140dfdb004134a388928 with the following minimal changes: diff (commits, branch) to add sufficient test coverage and avoid being trapped supporting a layout that seems opaque and misleading and I think likely to cause compatibility problems.
Feel free to ignore all other suggestions from me below. I think the other suggestions would simplify the PR, make the code more reliable, and fix minor bugs, but are not critical for compatibility or correctness. Only the ambiguous layout and lack of test coverage fixed by the suggested diff above hold me back from ACKing d8e024121b54fc998c61140dfdb004134a388928.
achow101 force-pushed
on Oct 7, 2020
achow101
commented at 7:08 pm on October 7, 2020:
member
I’ve done the suggested changes except for wallet.sqlite. I think we should discuss this during the wallet meeting this week.
I’ve also pulled in the suggested test changes.
achow101 force-pushed
on Oct 7, 2020
achow101 force-pushed
on Oct 7, 2020
achow101
commented at 8:36 pm on October 7, 2020:
member
I’ve done some testing with the behavior around a downgrading and different filenames.
Downgrading with the sqlite wallet named wallet.dat
Previous versions will attempt to open the wallet.dat file and fail to do so because the file magic does not match BDB’s. However on that failure, a salvage will be attempted. This will do nothing because the file magic does not match BDB’s. The file remains unchanged when this occurs and it fails with the error message Error: wallet.dat corrupt, salvage failed.
Downgrading with the sqlite wallet named wallet.sqlite
When attempting to load via loadwallet, the wallet will not be loaded as the RPC checks for existence of the wallet.dat file in the wallet’s directory. It fails with the error message Directory does not contain a wallet.dat file.
When specified with -wallet (either in cli options or bitcoin.conf), a new wallet.dat file will be created and used. This is a wholly new wallet unrelated to the wallet.sqlite file. There are no errors. This is dangerous.
For that last case, we either must keep the wallet.dat name, or come up with some workaround that prevents previous versions from creating a wallet.dat file.
The only possible solution I can think of is to create a dummy wallet.dat file to prevent previous versions from making a new file. However this is potentially dangerous because users may are expecting a wallet.dat file and may have tooling to interact with the wallet.dat file (such as creating and restoring backups) and thus they may be accidentally interacting with a useless dummy file instead of the real wallet.sqlite file. So I don’t think this is a good approach.
Furthermore, as mentioned previously, there is a lot of tooling and documentation involving the name wallet.dat. Users are expecting their wallet file to be named wallet.dat. While we have a backupwallet RPC and backup option in the GUI, many users probably don’t use these because there is “common knowledge” of directly backing up the wallet.dat file. And while copying the wallet.dat file while Bitcoin Core is running is not a supported use case, copying it after a clean shutdown is. Users may be doing that for their backups. Retaining the wallet.dat naming means that those tools and documentation are still valid.
in
src/wallet/sqlite.cpp:347
in
d993dee317outdated
d993dee317b93b11296282b9e97fa66e8c5ca152: TxnAbort() returns true if and only if ROLLBACK TRANSACTION succeeds, so I’m confused what you mean by “without the transaction being explicitly commited or aborted”
I’m confused what you mean by “without the transaction being explicitly commited or aborted”
It means that the caller didn’t call TxnAbort or TxnCommit and instead it is being done on the batch close.
in
src/wallet/sqlite.cpp:82
in
fe12f7946eoutdated
61+ std::string delete_sql = "DELETE FROM main WHERE key = ?";
62+ std::string cursor_sql = "SELECT key, value FROM main";
63+
64+ int res;
65+ if (!m_read_stmt) {
66+ if ((res = sqlite3_prepare_v2(m_database.m_db, read_sql.c_str(), -1, &m_read_stmt, nullptr)) != SQLITE_OK) {
dfa867392725f999a331404205635ee7a331886f maybe explicitly mention in EncryptWallet that SQLite also leaves data behind when you delete a row (docs say: “This can allow deleted content to be recovered by a hacker or by forensic analysis.”)
Maybe throw/assert that skip is not set, since the argument is ignored.