From 440d7f393d74f309d29e1d10ba1f11c41c869226 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 14:33:40 -0300 Subject: 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. --- src/db.cpp | 34 ++++++++++++++++++++++++++++++++++ src/db.h | 20 ++++++++++++++++++-- src/selftest.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/db.cpp b/src/db.cpp index 86f6dd7..c16edb4 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -164,6 +164,40 @@ bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey) return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); } +// Transaction bracket for the SQLite runtime. When the SQLite backend is not +// active these are no-ops that succeed, so a caller can wrap a group of related +// writes and Berkeley DB keeps its existing per-write behaviour unchanged while +// SQLite gets all-or-nothing. +bool WalletSQLiteRuntimeBeginTxn() +{ + if (!pWalletSQLiteRuntime) + return true; + string strError; + if (pWalletSQLiteRuntime->BeginTransaction(strError)) + return true; + printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str()); + return false; +} + +bool WalletSQLiteRuntimeCommitTxn() +{ + if (!pWalletSQLiteRuntime) + return true; + string strError; + if (pWalletSQLiteRuntime->CommitTransaction(strError)) + return true; + printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str()); + return false; +} + +void WalletSQLiteRuntimeRollbackTxn() +{ + if (!pWalletSQLiteRuntime) + return; + string strError; + pWalletSQLiteRuntime->RollbackTransaction(strError); +} + CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { diff --git a/src/db.h b/src/db.h index fee5a82..1a272b9 100644 --- a/src/db.h +++ b/src/db.h @@ -365,6 +365,9 @@ bool WalletSQLiteRuntimeWriteRecord(const vector& vchKey, const vector& vchValue, bool fOverwrite); bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey); +bool WalletSQLiteRuntimeBeginTxn(); +bool WalletSQLiteRuntimeCommitTxn(); +void WalletSQLiteRuntimeRollbackTxn(); @@ -531,8 +534,21 @@ public: bool WriteHDMaster(const vector& vchMaster, const vector& vchChainCode) { - return WriteWalletRecord(string("hdmaster"), vchMaster) - && WriteWalletRecord(string("hdchaincode"), vchChainCode); + // hdmaster and hdchaincode are halves of one seed: a wallet with one but + // not the other is an incomplete HD wallet (-walletstoragecheck flags + // exactly that). Under the SQLite runtime write them in one transaction + // so a failure or crash between the two cannot leave that half-state. + // Berkeley DB keeps its existing per-write behaviour -- the bracket is a + // no-op there. + if (!WalletSQLiteRuntimeBeginTxn()) + return false; + if (!WriteWalletRecord(string("hdmaster"), vchMaster) || + !WriteWalletRecord(string("hdchaincode"), vchChainCode)) + { + WalletSQLiteRuntimeRollbackTxn(); + return false; + } + return WalletSQLiteRuntimeCommitTxn(); } 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() nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), "active SQLite runtime does not create wallet.dat") ? 0 : 1; + // Transaction bracket: a rolled-back group leaves nothing, a + // committed group persists. This is the all-or-nothing that + // WriteHDMaster relies on so a half-written seed cannot survive + // a failure between its two records. + bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); + bool fTxnWrote = fTxnBegan && + CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); + if (fTxnBegan) + WalletSQLiteRuntimeRollbackTxn(); + nFail += Check(fTxnBegan && fTxnWrote, + "a SQLite runtime transaction opens and rolls back") ? 0 : 1; + int64 nRolledBack = 0; + nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", + nRolledBack), + "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; + + bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && + CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && + WalletSQLiteRuntimeCommitTxn(); + int64 nCommitted = 0; + nFail += Check(fCommitOk && + CWalletDB("r").ReadSetting("runtime-txn-commit", + nCommitted) && + nCommitted == 222, + "a committed SQLite runtime write persists") ? 0 : 1; + + // WriteHDMaster brackets its two records in that transaction; + // both halves of the seed must land. + vector vchTxnMaster(32, 0x5a); + vector vchTxnChainCode(32, 0xa5); + vector vchReadMaster, vchReadChainCode; + nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && + CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && + vchReadMaster == vchTxnMaster && + vchReadChainCode == vchTxnChainCode, + "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1; + // Durability: close the backend (checkpoints the WAL), reopen // the same file, and confirm what CWalletDB wrote is still // there. This is the write-survives-a-restart property the real -- cgit v1.2.3