diff options
| author | bitflash <dev@bitflash.network> | 2026-08-18 14:33:40 -0300 |
|---|---|---|
| committer | bitflash <dev@bitflash.network> | 2026-08-18 14:33:40 -0300 |
| commit | 440d7f393d74f309d29e1d10ba1f11c41c869226 (patch) | |
| tree | 40c881b79ba7a2571cfd2bcfbb274031a5739071 /src | |
| parent | e58c4022e0ddd137b93cdb3464753bef5df1d456 (diff) | |
wallet: bracket WriteHDMaster in a SQLite runtime transaction
The SQLite runtime wrote each record as its own autocommit, so a group
that must land together -- hdmaster and hdchaincode, the two halves of an
HD seed -- could be left half-written by a failure or crash between them,
the incomplete-seed state -walletstoragecheck exists to catch. Berkeley DB
was no worse (it is not transactional here either) but no better.
Add WalletSQLiteRuntimeBeginTxn/CommitTxn/RollbackTxn -- no-ops that
succeed when the SQLite backend is not active, so Berkeley DB keeps its
existing per-write behaviour -- and wrap WriteHDMaster in them so the seed
lands all-or-nothing under SQLite. The one BDB-atomic wallet rewrite, the
encrypt path, is already matched by its own transaction in the SQLite
encrypt branch; every other wallet multi-write is ordering-based on both
backends by design.
wallet-sqlite self-test now proves the bracket: a rolled-back runtime
write leaves no record, a committed one persists, and WriteHDMaster lands
both halves of the seed.
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.cpp | 34 | ||||
| -rw-r--r-- | src/db.h | 20 | ||||
| -rw-r--r-- | src/selftest.cpp | 37 |
3 files changed, 89 insertions, 2 deletions
| @@ -164,6 +164,40 @@ bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey) | |||
| 164 | return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); | 164 | return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); |
| 165 | } | 165 | } |
| 166 | 166 | ||
| 167 | // Transaction bracket for the SQLite runtime. When the SQLite backend is not | ||
| 168 | // active these are no-ops that succeed, so a caller can wrap a group of related | ||
| 169 | // writes and Berkeley DB keeps its existing per-write behaviour unchanged while | ||
| 170 | // SQLite gets all-or-nothing. | ||
| 171 | bool WalletSQLiteRuntimeBeginTxn() | ||
| 172 | { | ||
| 173 | if (!pWalletSQLiteRuntime) | ||
| 174 | return true; | ||
| 175 | string strError; | ||
| 176 | if (pWalletSQLiteRuntime->BeginTransaction(strError)) | ||
| 177 | return true; | ||
| 178 | printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str()); | ||
| 179 | return false; | ||
| 180 | } | ||
| 181 | |||
| 182 | bool WalletSQLiteRuntimeCommitTxn() | ||
| 183 | { | ||
| 184 | if (!pWalletSQLiteRuntime) | ||
| 185 | return true; | ||
| 186 | string strError; | ||
| 187 | if (pWalletSQLiteRuntime->CommitTransaction(strError)) | ||
| 188 | return true; | ||
| 189 | printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str()); | ||
| 190 | return false; | ||
| 191 | } | ||
| 192 | |||
| 193 | void WalletSQLiteRuntimeRollbackTxn() | ||
| 194 | { | ||
| 195 | if (!pWalletSQLiteRuntime) | ||
| 196 | return; | ||
| 197 | string strError; | ||
| 198 | pWalletSQLiteRuntime->RollbackTransaction(strError); | ||
| 199 | } | ||
| 200 | |||
| 167 | 201 | ||
| 168 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | 202 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) |
| 169 | { | 203 | { |
| @@ -365,6 +365,9 @@ bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey, | |||
| 365 | const vector<unsigned char>& vchValue, | 365 | const vector<unsigned char>& vchValue, |
| 366 | bool fOverwrite); | 366 | bool fOverwrite); |
| 367 | bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey); | 367 | bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey); |
| 368 | bool WalletSQLiteRuntimeBeginTxn(); | ||
| 369 | bool WalletSQLiteRuntimeCommitTxn(); | ||
| 370 | void WalletSQLiteRuntimeRollbackTxn(); | ||
| 368 | 371 | ||
| 369 | 372 | ||
| 370 | 373 | ||
| @@ -531,8 +534,21 @@ public: | |||
| 531 | 534 | ||
| 532 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) | 535 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) |
| 533 | { | 536 | { |
| 534 | return WriteWalletRecord(string("hdmaster"), vchMaster) | 537 | // hdmaster and hdchaincode are halves of one seed: a wallet with one but |
| 535 | && WriteWalletRecord(string("hdchaincode"), vchChainCode); | 538 | // not the other is an incomplete HD wallet (-walletstoragecheck flags |
| 539 | // exactly that). Under the SQLite runtime write them in one transaction | ||
| 540 | // so a failure or crash between the two cannot leave that half-state. | ||
| 541 | // Berkeley DB keeps its existing per-write behaviour -- the bracket is a | ||
| 542 | // no-op there. | ||
| 543 | if (!WalletSQLiteRuntimeBeginTxn()) | ||
| 544 | return false; | ||
| 545 | if (!WriteWalletRecord(string("hdmaster"), vchMaster) || | ||
| 546 | !WriteWalletRecord(string("hdchaincode"), vchChainCode)) | ||
| 547 | { | ||
| 548 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 549 | return false; | ||
| 550 | } | ||
| 551 | return WalletSQLiteRuntimeCommitTxn(); | ||
| 536 | } | 552 | } |
| 537 | 553 | ||
| 538 | bool WriteWalletMinVersion(int nVersion) | 554 | bool WriteWalletMinVersion(int nVersion) |
diff --git a/src/selftest.cpp b/src/selftest.cpp index fc10947..7ad5a53 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -1231,6 +1231,43 @@ static int RunWalletSQLiteSelfTest() | |||
| 1231 | nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), | 1231 | nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), |
| 1232 | "active SQLite runtime does not create wallet.dat") ? 0 : 1; | 1232 | "active SQLite runtime does not create wallet.dat") ? 0 : 1; |
| 1233 | 1233 | ||
| 1234 | // Transaction bracket: a rolled-back group leaves nothing, a | ||
| 1235 | // committed group persists. This is the all-or-nothing that | ||
| 1236 | // WriteHDMaster relies on so a half-written seed cannot survive | ||
| 1237 | // a failure between its two records. | ||
| 1238 | bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); | ||
| 1239 | bool fTxnWrote = fTxnBegan && | ||
| 1240 | CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); | ||
| 1241 | if (fTxnBegan) | ||
| 1242 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 1243 | nFail += Check(fTxnBegan && fTxnWrote, | ||
| 1244 | "a SQLite runtime transaction opens and rolls back") ? 0 : 1; | ||
| 1245 | int64 nRolledBack = 0; | ||
| 1246 | nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", | ||
| 1247 | nRolledBack), | ||
| 1248 | "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; | ||
| 1249 | |||
| 1250 | bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && | ||
| 1251 | CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && | ||
| 1252 | WalletSQLiteRuntimeCommitTxn(); | ||
| 1253 | int64 nCommitted = 0; | ||
| 1254 | nFail += Check(fCommitOk && | ||
| 1255 | CWalletDB("r").ReadSetting("runtime-txn-commit", | ||
| 1256 | nCommitted) && | ||
| 1257 | nCommitted == 222, | ||
| 1258 | "a committed SQLite runtime write persists") ? 0 : 1; | ||
| 1259 | |||
| 1260 | // WriteHDMaster brackets its two records in that transaction; | ||
| 1261 | // both halves of the seed must land. | ||
| 1262 | vector<unsigned char> vchTxnMaster(32, 0x5a); | ||
| 1263 | vector<unsigned char> vchTxnChainCode(32, 0xa5); | ||
| 1264 | vector<unsigned char> vchReadMaster, vchReadChainCode; | ||
| 1265 | nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && | ||
| 1266 | CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && | ||
| 1267 | vchReadMaster == vchTxnMaster && | ||
| 1268 | vchReadChainCode == vchTxnChainCode, | ||
| 1269 | "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1; | ||
| 1270 | |||
| 1234 | // Durability: close the backend (checkpoints the WAL), reopen | 1271 | // Durability: close the backend (checkpoints the WAL), reopen |
| 1235 | // the same file, and confirm what CWalletDB wrote is still | 1272 | // the same file, and confirm what CWalletDB wrote is still |
| 1236 | // there. This is the write-survives-a-restart property the real | 1273 | // there. This is the write-survives-a-restart property the real |