diff options
| author | Bitflash <admin@bitflash.network> | 2026-08-18 15:48:17 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-18 15:48:17 -0300 |
| commit | f3fb7ce6759ac98a7cc39d586fc2824982c74acd (patch) | |
| tree | 23e39a26b340324a7a33a115aded3b70b5332816 /src | |
| parent | beb5c1435a0d85b7b24d4d371933758ebed90907 (diff) | |
| parent | 84a60275569dca5d1fb12db38ce184caf7487ed6 (diff) | |
Merge pull request 'wallet: complete SQLite backend activation (encrypt, atomicity, one-shot commands)' (#61) from wallet-sqlite-encrypt into main
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.cpp | 356 | ||||
| -rw-r--r-- | src/db.h | 22 | ||||
| -rw-r--r-- | src/main_gui.cpp | 50 | ||||
| -rw-r--r-- | src/makefile.mingw | 1 | ||||
| -rw-r--r-- | src/selftest.cpp | 286 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 15 |
6 files changed, 717 insertions, 13 deletions
| @@ -164,6 +164,72 @@ 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 | |||
| 201 | // Open the SQLite wallet read-only as the active runtime. Used by the one-shot | ||
| 202 | // storage diagnostics, which run before the normal wallet load and must be able | ||
| 203 | // to inspect a wallet that the loader would refuse -- so this opens the file for | ||
| 204 | // scanning without loading or validating its records. | ||
| 205 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet) | ||
| 206 | { | ||
| 207 | strErrorRet.clear(); | ||
| 208 | if (!FileExists(strPath.c_str())) | ||
| 209 | { | ||
| 210 | strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str()); | ||
| 211 | return false; | ||
| 212 | } | ||
| 213 | |||
| 214 | WalletSQLiteRuntimeClose(); | ||
| 215 | std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite()); | ||
| 216 | if (!db->OpenReadOnly(strPath, strErrorRet)) | ||
| 217 | return false; | ||
| 218 | |||
| 219 | pWalletSQLiteRuntime = db.release(); | ||
| 220 | return true; | ||
| 221 | } | ||
| 222 | |||
| 223 | bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet) | ||
| 224 | { | ||
| 225 | if (!pWalletSQLiteRuntime) | ||
| 226 | { | ||
| 227 | strErrorRet = "SQLite wallet backend is not active"; | ||
| 228 | return false; | ||
| 229 | } | ||
| 230 | return pWalletSQLiteRuntime->Checkpoint(strErrorRet); | ||
| 231 | } | ||
| 232 | |||
| 167 | 233 | ||
| 168 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | 234 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) |
| 169 | { | 235 | { |
| @@ -1320,6 +1386,16 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 1320 | 1386 | ||
| 1321 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) | 1387 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) |
| 1322 | { | 1388 | { |
| 1389 | // Under the SQLite backend there is no Berkeley DB cursor to walk; scan the | ||
| 1390 | // active SQLite store instead. This is the single choke point every "read | ||
| 1391 | // every wallet record" caller goes through (storage audit and check among | ||
| 1392 | // them), so routing it here makes those honour -walletbackend=sqlite. | ||
| 1393 | if (WalletSQLiteRuntimeActive()) | ||
| 1394 | { | ||
| 1395 | strErrorRet.clear(); | ||
| 1396 | return pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); | ||
| 1397 | } | ||
| 1398 | |||
| 1323 | class CWalletScanDB : public CWalletDB | 1399 | class CWalletScanDB : public CWalletDB |
| 1324 | { | 1400 | { |
| 1325 | public: | 1401 | public: |
| @@ -1550,11 +1626,81 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath) | |||
| 1550 | // this file was written are not spendable from it. Backing up once is not | 1626 | // this file was written are not spendable from it. Backing up once is not |
| 1551 | // enough, and that is a property of the wallet format rather than of this | 1627 | // enough, and that is a property of the wallet format rather than of this |
| 1552 | // function. | 1628 | // function. |
| 1629 | // Back up the active SQLite wallet. The store is checkpointed so the WAL is | ||
| 1630 | // folded into the main file, then the single self-contained wallet.sqlite is | ||
| 1631 | // copied through a temp file and renamed into place (same temp/old/rename dance | ||
| 1632 | // as the Berkeley DB path, for Windows' non-overwriting rename). No lsn_reset: | ||
| 1633 | // a checkpointed SQLite file needs nothing beside it to open. | ||
| 1634 | static bool BackupWalletSQLite(const string& strDest) | ||
| 1635 | { | ||
| 1636 | string strSrc = GetAppDir() + "/wallet.sqlite"; | ||
| 1637 | if (!FileExists(strSrc.c_str())) | ||
| 1638 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); | ||
| 1639 | |||
| 1640 | string strCheckpointError; | ||
| 1641 | if (!WalletSQLiteRuntimeCheckpoint(strCheckpointError)) | ||
| 1642 | return error("BackupWallet() : checkpoint failed: %s\n", strCheckpointError.c_str()); | ||
| 1643 | |||
| 1644 | string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime()); | ||
| 1645 | for (int i = 1; FileExists(strTemp.c_str()); i++) | ||
| 1646 | strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1647 | string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime()); | ||
| 1648 | for (int i = 1; FileExists(strOld.c_str()); i++) | ||
| 1649 | strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1650 | |||
| 1651 | FILE* pfIn = fopen(strSrc.c_str(), "rb"); | ||
| 1652 | if (!pfIn) | ||
| 1653 | return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); | ||
| 1654 | FILE* pfOut = fopen(strTemp.c_str(), "wb"); | ||
| 1655 | if (!pfOut) | ||
| 1656 | { | ||
| 1657 | fclose(pfIn); | ||
| 1658 | return error("BackupWallet() : cannot write %s\n", strTemp.c_str()); | ||
| 1659 | } | ||
| 1660 | |||
| 1661 | char buf[65536]; | ||
| 1662 | size_t n; | ||
| 1663 | bool fOk = true; | ||
| 1664 | while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0) | ||
| 1665 | if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; } | ||
| 1666 | if (ferror(pfIn)) | ||
| 1667 | fOk = false; | ||
| 1668 | fclose(pfIn); | ||
| 1669 | if (fclose(pfOut) != 0) | ||
| 1670 | fOk = false; | ||
| 1671 | if (!fOk) | ||
| 1672 | { | ||
| 1673 | remove(strTemp.c_str()); // half a wallet is worse than none | ||
| 1674 | return error("BackupWallet() : copy to %s failed\n", strTemp.c_str()); | ||
| 1675 | } | ||
| 1676 | |||
| 1677 | bool fHadOldDest = FileExists(strDest.c_str()); | ||
| 1678 | if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0) | ||
| 1679 | { | ||
| 1680 | remove(strTemp.c_str()); | ||
| 1681 | return error("BackupWallet() : could not move old %s aside\n", strDest.c_str()); | ||
| 1682 | } | ||
| 1683 | if (rename(strTemp.c_str(), strDest.c_str()) != 0) | ||
| 1684 | { | ||
| 1685 | remove(strTemp.c_str()); | ||
| 1686 | if (fHadOldDest) | ||
| 1687 | rename(strOld.c_str(), strDest.c_str()); | ||
| 1688 | return error("BackupWallet() : could not install %s\n", strDest.c_str()); | ||
| 1689 | } | ||
| 1690 | if (fHadOldDest) | ||
| 1691 | remove(strOld.c_str()); | ||
| 1692 | return true; | ||
| 1693 | } | ||
| 1694 | |||
| 1553 | bool BackupWallet(const string& strDest) | 1695 | bool BackupWallet(const string& strDest) |
| 1554 | { | 1696 | { |
| 1555 | if (strDest.empty()) | 1697 | if (strDest.empty()) |
| 1556 | return error("BackupWallet() : no destination given\n"); | 1698 | return error("BackupWallet() : no destination given\n"); |
| 1557 | 1699 | ||
| 1700 | // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat. | ||
| 1701 | if (WalletSQLiteRuntimeActive()) | ||
| 1702 | return BackupWalletSQLite(strDest); | ||
| 1703 | |||
| 1558 | string strSrc = GetAppDir() + "/wallet.dat"; | 1704 | string strSrc = GetAppDir() + "/wallet.dat"; |
| 1559 | if (!FileExists(strSrc.c_str())) | 1705 | if (!FileExists(strSrc.c_str())) |
| 1560 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); | 1706 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); |
| @@ -1974,9 +2120,9 @@ static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo, | |||
| 1974 | return ScanWalletRecords(visitor, strErrorRet); | 2120 | return ScanWalletRecords(visitor, strErrorRet); |
| 1975 | } | 2121 | } |
| 1976 | 2122 | ||
| 1977 | static string WalletEncryptBackupPath() | 2123 | static string WalletEncryptBackupPathFor(const char* pszBaseName) |
| 1978 | { | 2124 | { |
| 1979 | string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; | 2125 | string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt."; |
| 1980 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; | 2126 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; |
| 1981 | for (int i = 1; FileExists(strPath.c_str()); i++) | 2127 | for (int i = 1; FileExists(strPath.c_str()); i++) |
| 1982 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; | 2128 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; |
| @@ -2006,6 +2152,192 @@ static void SecureClearBytes(vector<unsigned char>& vch) | |||
| 2006 | vch.clear(); | 2152 | vch.clear(); |
| 2007 | } | 2153 | } |
| 2008 | 2154 | ||
| 2155 | // The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list, | ||
| 2156 | // same byte-for-byte copy, but the destination is a CWalletDBSQLite record | ||
| 2157 | // store rather than a Berkeley DB file. | ||
| 2158 | class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor | ||
| 2159 | { | ||
| 2160 | public: | ||
| 2161 | CWalletDBSQLite& dbTo; | ||
| 2162 | int& nCopied; | ||
| 2163 | |||
| 2164 | CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn) | ||
| 2165 | : dbTo(dbToIn), nCopied(nCopiedIn) { } | ||
| 2166 | |||
| 2167 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 2168 | const CDataStream& ssValue, | ||
| 2169 | string& strErrorRet) | ||
| 2170 | { | ||
| 2171 | string strType; | ||
| 2172 | try | ||
| 2173 | { | ||
| 2174 | CDataStream ssType = ssKey; | ||
| 2175 | ssType >> strType; | ||
| 2176 | } | ||
| 2177 | catch (...) | ||
| 2178 | { | ||
| 2179 | strErrorRet = "SQLite wallet has a record whose key type cannot be read"; | ||
| 2180 | return false; | ||
| 2181 | } | ||
| 2182 | |||
| 2183 | if (IsWalletEncryptionPrivateRecord(strType)) | ||
| 2184 | return true; | ||
| 2185 | |||
| 2186 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 2187 | vector<unsigned char> vchValue(ssValue.begin(), ssValue.end()); | ||
| 2188 | bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true); | ||
| 2189 | if (!vchKey.empty()) | ||
| 2190 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 2191 | if (!vchValue.empty()) | ||
| 2192 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 2193 | if (!fOk) | ||
| 2194 | { | ||
| 2195 | strErrorRet = strprintf("could not copy wallet record '%s': %s", | ||
| 2196 | strType.c_str(), strErrorRet.c_str()); | ||
| 2197 | return false; | ||
| 2198 | } | ||
| 2199 | nCopied++; | ||
| 2200 | return true; | ||
| 2201 | } | ||
| 2202 | }; | ||
| 2203 | |||
| 2204 | // Persist an encrypted wallet under the SQLite backend. Everything the caller | ||
| 2205 | // computed -- the master key, the re-encrypted private keys, the fresh crypted | ||
| 2206 | // key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built | ||
| 2207 | // in one transaction, which is then swapped in for the plaintext original. | ||
| 2208 | // | ||
| 2209 | // A fresh file rather than an in-place rewrite for two reasons: it keeps the | ||
| 2210 | // whole change atomic (a crash before the rename leaves the old wallet whole), | ||
| 2211 | // and it guarantees no plaintext key survives in a freed page the way an | ||
| 2212 | // in-place DELETE would leave behind until a VACUUM. | ||
| 2213 | static bool EncryptWalletPersistSQLite( | ||
| 2214 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vCryptedKeys, | ||
| 2215 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vNewPoolKeys, | ||
| 2216 | const vector<pair<int64, vector<unsigned char> > >& vNewPool, | ||
| 2217 | const CWalletMasterKey& kMasterKey, | ||
| 2218 | const vector<unsigned char>& vchCryptedHDMasterNew, | ||
| 2219 | const vector<unsigned char>& vchCryptedHDChainCodeNew, | ||
| 2220 | unsigned int nHDNextNew, | ||
| 2221 | int& nCopiedRet, | ||
| 2222 | string& strBackupRet, | ||
| 2223 | string& strErrorRet) | ||
| 2224 | { | ||
| 2225 | nCopiedRet = 0; | ||
| 2226 | if (!pWalletSQLiteRuntime) | ||
| 2227 | { | ||
| 2228 | strErrorRet = "SQLite wallet backend is not active"; | ||
| 2229 | return false; | ||
| 2230 | } | ||
| 2231 | |||
| 2232 | string strWalletPath = GetAppDir() + "/wallet.sqlite"; | ||
| 2233 | string strTempPath = GetAppDir() + "/" + | ||
| 2234 | strprintf("wallet.sqlite.encrypting.%lld", GetTime()); | ||
| 2235 | remove(strTempPath.c_str()); | ||
| 2236 | remove((strTempPath + "-wal").c_str()); | ||
| 2237 | remove((strTempPath + "-shm").c_str()); | ||
| 2238 | |||
| 2239 | { | ||
| 2240 | CWalletDBSQLite dbTmp; | ||
| 2241 | if (!dbTmp.Open(strTempPath, strErrorRet)) | ||
| 2242 | { | ||
| 2243 | remove(strTempPath.c_str()); | ||
| 2244 | return false; | ||
| 2245 | } | ||
| 2246 | if (!dbTmp.BeginTransaction(strErrorRet)) | ||
| 2247 | { | ||
| 2248 | dbTmp.Close(); | ||
| 2249 | remove(strTempPath.c_str()); | ||
| 2250 | return false; | ||
| 2251 | } | ||
| 2252 | |||
| 2253 | // The public records come from the live SQLite store -- under this | ||
| 2254 | // backend ScanWalletRecords() has no Berkeley DB behind it to read. | ||
| 2255 | CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet); | ||
| 2256 | bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); | ||
| 2257 | |||
| 2258 | // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB | ||
| 2259 | // path (CWalletRewriteDB) writes it, so a wallet encrypted under either | ||
| 2260 | // backend is the same set of records. ckey uses INSERT-only to match | ||
| 2261 | // WriteCryptedKey; the rest overwrite. | ||
| 2262 | fOk = fOk && | ||
| 2263 | dbTmp.WriteTypedRecord(string("walletminversion"), | ||
| 2264 | (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) && | ||
| 2265 | dbTmp.WriteTypedRecord(make_pair(string("mkey"), | ||
| 2266 | (unsigned int)(nWalletMasterKeyMaxID + 1)), | ||
| 2267 | kMasterKey, strErrorRet, true); | ||
| 2268 | for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++) | ||
| 2269 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first), | ||
| 2270 | vCryptedKeys[i].second, strErrorRet, false); | ||
| 2271 | for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++) | ||
| 2272 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first), | ||
| 2273 | vNewPoolKeys[i].second, strErrorRet, false); | ||
| 2274 | if (fOk && !vchCryptedHDMasterNew.empty()) | ||
| 2275 | fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"), | ||
| 2276 | vchCryptedHDMasterNew, strErrorRet, true) && | ||
| 2277 | dbTmp.WriteTypedRecord(string("cryptedhdchaincode"), | ||
| 2278 | vchCryptedHDChainCodeNew, strErrorRet, true); | ||
| 2279 | if (fOk) | ||
| 2280 | fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true); | ||
| 2281 | for (size_t i = 0; fOk && i < vNewPool.size(); i++) | ||
| 2282 | fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first), | ||
| 2283 | vNewPool[i].second, strErrorRet, true); | ||
| 2284 | |||
| 2285 | if (!fOk) | ||
| 2286 | { | ||
| 2287 | string strIgnore; | ||
| 2288 | dbTmp.RollbackTransaction(strIgnore); | ||
| 2289 | dbTmp.Close(); | ||
| 2290 | remove(strTempPath.c_str()); | ||
| 2291 | remove((strTempPath + "-wal").c_str()); | ||
| 2292 | remove((strTempPath + "-shm").c_str()); | ||
| 2293 | return false; | ||
| 2294 | } | ||
| 2295 | |||
| 2296 | if (!dbTmp.CommitTransaction(strErrorRet)) | ||
| 2297 | { | ||
| 2298 | dbTmp.Close(); | ||
| 2299 | remove(strTempPath.c_str()); | ||
| 2300 | remove((strTempPath + "-wal").c_str()); | ||
| 2301 | remove((strTempPath + "-shm").c_str()); | ||
| 2302 | return false; | ||
| 2303 | } | ||
| 2304 | string strCheckpointError; | ||
| 2305 | dbTmp.Checkpoint(strCheckpointError); | ||
| 2306 | dbTmp.Close(); | ||
| 2307 | } | ||
| 2308 | |||
| 2309 | // The rebuilt wallet is committed and self-contained. Close the live handle | ||
| 2310 | // so the file can be swapped: Windows will not rename a file that still has | ||
| 2311 | // an open handle, and the node exits right after this. | ||
| 2312 | WalletSQLiteRuntimeClose(); | ||
| 2313 | |||
| 2314 | string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite"); | ||
| 2315 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | ||
| 2316 | { | ||
| 2317 | strErrorRet = strprintf("could not move the original SQLite wallet to %s", | ||
| 2318 | strBackupPath.c_str()); | ||
| 2319 | remove(strTempPath.c_str()); | ||
| 2320 | return false; | ||
| 2321 | } | ||
| 2322 | // The moved-aside wallet owns any leftover write-ahead log or shared-memory | ||
| 2323 | // sidecar; drop stale ones so they cannot shadow the new wallet. | ||
| 2324 | remove((strWalletPath + "-wal").c_str()); | ||
| 2325 | remove((strWalletPath + "-shm").c_str()); | ||
| 2326 | |||
| 2327 | if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) | ||
| 2328 | { | ||
| 2329 | rename(strBackupPath.c_str(), strWalletPath.c_str()); | ||
| 2330 | strErrorRet = "could not install the encrypted SQLite wallet; the original was restored"; | ||
| 2331 | remove(strTempPath.c_str()); | ||
| 2332 | return false; | ||
| 2333 | } | ||
| 2334 | remove((strTempPath + "-wal").c_str()); | ||
| 2335 | remove((strTempPath + "-shm").c_str()); | ||
| 2336 | |||
| 2337 | strBackupRet = strBackupPath; | ||
| 2338 | return true; | ||
| 2339 | } | ||
| 2340 | |||
| 2009 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) | 2341 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) |
| 2010 | { | 2342 | { |
| 2011 | strBackupRet.clear(); | 2343 | strBackupRet.clear(); |
| @@ -2144,6 +2476,24 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 2144 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); | 2476 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); |
| 2145 | } | 2477 | } |
| 2146 | 2478 | ||
| 2479 | // Every ciphertext is computed; nothing has touched the store yet. Under | ||
| 2480 | // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic | ||
| 2481 | // transaction and swapping it in. The Berkeley DB path below is unchanged. | ||
| 2482 | if (WalletSQLiteRuntimeActive()) | ||
| 2483 | { | ||
| 2484 | int nCopiedSQLite = 0; | ||
| 2485 | if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool, | ||
| 2486 | kMasterKey, vchCryptedHDMasterNew, | ||
| 2487 | vchCryptedHDChainCodeNew, nHDNextNew, | ||
| 2488 | nCopiedSQLite, strBackupRet, strErrorRet)) | ||
| 2489 | return false; | ||
| 2490 | printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " | ||
| 2491 | "rebuilt %d key-pool entry(s) [SQLite]\n", | ||
| 2492 | nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), | ||
| 2493 | (int)vNewPool.size()); | ||
| 2494 | return true; | ||
| 2495 | } | ||
| 2496 | |||
| 2147 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); | 2497 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); |
| 2148 | string strTempPath = GetAppDir() + "/" + strTempFile; | 2498 | string strTempPath = GetAppDir() + "/" + strTempFile; |
| 2149 | remove(strTempPath.c_str()); | 2499 | remove(strTempPath.c_str()); |
| @@ -2211,7 +2561,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 2211 | DBFlush(true); | 2561 | DBFlush(true); |
| 2212 | 2562 | ||
| 2213 | string strWalletPath = GetAppDir() + "/wallet.dat"; | 2563 | string strWalletPath = GetAppDir() + "/wallet.dat"; |
| 2214 | string strBackupPath = WalletEncryptBackupPath(); | 2564 | string strBackupPath = WalletEncryptBackupPathFor("wallet.dat"); |
| 2215 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | 2565 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) |
| 2216 | { | 2566 | { |
| 2217 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); | 2567 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); |
| @@ -365,6 +365,11 @@ 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(); | ||
| 371 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); | ||
| 372 | bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); | ||
| 368 | 373 | ||
| 369 | 374 | ||
| 370 | 375 | ||
| @@ -531,8 +536,21 @@ public: | |||
| 531 | 536 | ||
| 532 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) | 537 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) |
| 533 | { | 538 | { |
| 534 | return WriteWalletRecord(string("hdmaster"), vchMaster) | 539 | // hdmaster and hdchaincode are halves of one seed: a wallet with one but |
| 535 | && WriteWalletRecord(string("hdchaincode"), vchChainCode); | 540 | // not the other is an incomplete HD wallet (-walletstoragecheck flags |
| 541 | // exactly that). Under the SQLite runtime write them in one transaction | ||
| 542 | // so a failure or crash between the two cannot leave that half-state. | ||
| 543 | // Berkeley DB keeps its existing per-write behaviour -- the bracket is a | ||
| 544 | // no-op there. | ||
| 545 | if (!WalletSQLiteRuntimeBeginTxn()) | ||
| 546 | return false; | ||
| 547 | if (!WriteWalletRecord(string("hdmaster"), vchMaster) || | ||
| 548 | !WriteWalletRecord(string("hdchaincode"), vchChainCode)) | ||
| 549 | { | ||
| 550 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 551 | return false; | ||
| 552 | } | ||
| 553 | return WalletSQLiteRuntimeCommitTxn(); | ||
| 536 | } | 554 | } |
| 537 | 555 | ||
| 538 | bool WriteWalletMinVersion(int nVersion) | 556 | bool WriteWalletMinVersion(int nVersion) |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 4be4995..af6053f 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -524,19 +524,57 @@ int main(int argc, char* argv[]) | |||
| 524 | // explain wallet.dat states that the normal wallet loader may refuse. | 524 | // explain wallet.dat states that the normal wallet loader may refuse. |
| 525 | string strWalletStorageAuditJson = | 525 | string strWalletStorageAuditJson = |
| 526 | argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); | 526 | argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); |
| 527 | if (arg(argc,argv,"/walletstorageaudit") || | 527 | bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") || |
| 528 | arg(argc,argv,"-walletstorageaudit") || | 528 | arg(argc,argv,"-walletstorageaudit") || |
| 529 | !strWalletStorageAuditJson.empty()) | 529 | !strWalletStorageAuditJson.empty(); |
| 530 | bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") || | ||
| 531 | arg(argc,argv,"-walletstoragecheck"); | ||
| 532 | |||
| 533 | // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite | ||
| 534 | // the live wallet is wallet.sqlite, so open it read-only as the active | ||
| 535 | // store and let the same scan walk the right file. This runs before the | ||
| 536 | // normal backend resolution further down, so validate the value here too. | ||
| 537 | string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend"); | ||
| 538 | if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() && | ||
| 539 | strDiagBackend != "sqlite" && strDiagBackend != "bdb") | ||
| 540 | { | ||
| 541 | AttachTerminal(); | ||
| 542 | fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite " | ||
| 543 | "or -walletbackend=bdb.\n", strDiagBackend.c_str()); | ||
| 544 | return 1; | ||
| 545 | } | ||
| 546 | bool fDiagSQLite = (strDiagBackend == "sqlite") && | ||
| 547 | (fDiagWantsAudit || fDiagWantsCheck); | ||
| 548 | if (fDiagSQLite) | ||
| 549 | { | ||
| 550 | AttachTerminal(); | ||
| 551 | string strSQLitePath = GetAppDir() + "/wallet.sqlite"; | ||
| 552 | string strOpenErr; | ||
| 553 | if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr)) | ||
| 554 | { | ||
| 555 | fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n", | ||
| 556 | strOpenErr.c_str()); | ||
| 557 | return 1; | ||
| 558 | } | ||
| 559 | } | ||
| 560 | |||
| 561 | if (fDiagWantsAudit) | ||
| 530 | { | 562 | { |
| 531 | int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); | 563 | int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); |
| 532 | DBFlush(true); | 564 | if (fDiagSQLite) |
| 565 | WalletSQLiteRuntimeClose(); | ||
| 566 | else | ||
| 567 | DBFlush(true); | ||
| 533 | return nRet; | 568 | return nRet; |
| 534 | } | 569 | } |
| 535 | 570 | ||
| 536 | if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) | 571 | if (fDiagWantsCheck) |
| 537 | { | 572 | { |
| 538 | int nRet = CmdWalletStorageCheck(); | 573 | int nRet = CmdWalletStorageCheck(); |
| 539 | DBFlush(true); | 574 | if (fDiagSQLite) |
| 575 | WalletSQLiteRuntimeClose(); | ||
| 576 | else | ||
| 577 | DBFlush(true); | ||
| 540 | return nRet; | 578 | return nRet; |
| 541 | } | 579 | } |
| 542 | 580 | ||
diff --git a/src/makefile.mingw b/src/makefile.mingw index 14e6b32..1cb47fb 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -112,6 +112,7 @@ tests: test_bip32.exe bitflash.exe | |||
| 112 | ./bitflash.exe -selftest=wallet-sqlite-migration | 112 | ./bitflash.exe -selftest=wallet-sqlite-migration |
| 113 | ./bitflash.exe -selftest=wallet-crypto | 113 | ./bitflash.exe -selftest=wallet-crypto |
| 114 | ./bitflash.exe -selftest=wallet-encrypt | 114 | ./bitflash.exe -selftest=wallet-encrypt |
| 115 | ./bitflash.exe -selftest=wallet-sqlite-encrypt | ||
| 115 | ./bitflash.exe -selftest=net-message | 116 | ./bitflash.exe -selftest=net-message |
| 116 | ./bitflash.exe -selftest=consensus-limits | 117 | ./bitflash.exe -selftest=consensus-limits |
| 117 | ./bitflash.exe -selftest=pool-stratum | 118 | ./bitflash.exe -selftest=pool-stratum |
diff --git a/src/selftest.cpp b/src/selftest.cpp index 098eed6..90a30d2 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 |
| @@ -2252,6 +2289,251 @@ static int RunWalletEncryptSelfTest() | |||
| 2252 | return nFail == 0 ? 0 : 1; | 2289 | return nFail == 0 ? 0 : 1; |
| 2253 | } | 2290 | } |
| 2254 | 2291 | ||
| 2292 | // Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley | ||
| 2293 | // DB encrypt test above, this drives the real command-line path end to end in | ||
| 2294 | // child processes: build a Berkeley DB wallet, export it to SQLite, then run | ||
| 2295 | // -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out | ||
| 2296 | // encrypted, unlockable, and with wallet.dat left untouched. | ||
| 2297 | static int RunWalletSQLiteEncryptSelfTest() | ||
| 2298 | { | ||
| 2299 | fflush(stdout); | ||
| 2300 | printf("wallet-sqlite-encrypt self-test\n"); | ||
| 2301 | |||
| 2302 | std::string tmp; | ||
| 2303 | std::string cwd; | ||
| 2304 | if (!MakeTempDir(tmp)) | ||
| 2305 | { | ||
| 2306 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2307 | return 1; | ||
| 2308 | } | ||
| 2309 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2310 | { | ||
| 2311 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2312 | RemoveTree(tmp); | ||
| 2313 | return 1; | ||
| 2314 | } | ||
| 2315 | |||
| 2316 | int nFail = 0; | ||
| 2317 | strSetDataDir = tmp; | ||
| 2318 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2319 | |||
| 2320 | try | ||
| 2321 | { | ||
| 2322 | // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, | ||
| 2323 | // and a couple of public records, then release it. Everything after this | ||
| 2324 | // runs the real command-line paths in child processes. | ||
| 2325 | if (!LoadWallet()) | ||
| 2326 | throw std::runtime_error("LoadWallet failed"); | ||
| 2327 | |||
| 2328 | string strError; | ||
| 2329 | const string strMnemonic = | ||
| 2330 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; | ||
| 2331 | nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), | ||
| 2332 | "a phrase can be installed before encryption") ? 0 : 1; | ||
| 2333 | TopUpKeyPool(); | ||
| 2334 | |||
| 2335 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2336 | CKey publicLabelKey; | ||
| 2337 | publicLabelKey.MakeNewKey(); | ||
| 2338 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2339 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2340 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2341 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2342 | |||
| 2343 | CPrivKey vchDefaultPrivKey; | ||
| 2344 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2345 | "the default private key is readable before encryption") ? 0 : 1; | ||
| 2346 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); | ||
| 2347 | vector<unsigned char> vchPlainHDMaster = vchHDMaster; | ||
| 2348 | vector<unsigned char> vchPlainHDChainCode = vchHDChainCode; | ||
| 2349 | |||
| 2350 | // Release wallet.dat so a child process can read it for the export. | ||
| 2351 | DBFlush(true); | ||
| 2352 | |||
| 2353 | #ifdef _WIN32 | ||
| 2354 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2355 | #else | ||
| 2356 | string strExe = cwd + "/bitflash-node"; | ||
| 2357 | #endif | ||
| 2358 | string strSQLitePath = tmp + "/wallet.sqlite"; | ||
| 2359 | string strWalletDatPath = tmp + "/wallet.dat"; | ||
| 2360 | string strRightPassFile = tmp + "/right-pass.txt"; | ||
| 2361 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | ||
| 2362 | nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && | ||
| 2363 | WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), | ||
| 2364 | "passphrase files can be written for restarted commands") ? 0 : 1; | ||
| 2365 | |||
| 2366 | // Step 1: export the Berkeley DB wallet to SQLite. | ||
| 2367 | vector<string> vExportArgs; | ||
| 2368 | vExportArgs.push_back("-datadir=" + tmp); | ||
| 2369 | vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); | ||
| 2370 | vExportArgs.push_back("-nogui"); | ||
| 2371 | int nExportRet = RunBitflashChild(strExe, vExportArgs); | ||
| 2372 | nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), | ||
| 2373 | "the wallet exports to a SQLite file") ? 0 : 1; | ||
| 2374 | nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2375 | "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; | ||
| 2376 | |||
| 2377 | // Step 2: encrypt through the real SQLite backend command path. | ||
| 2378 | string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; | ||
| 2379 | vector<string> vEncryptArgs; | ||
| 2380 | vEncryptArgs.push_back("-datadir=" + tmp); | ||
| 2381 | vEncryptArgs.push_back("-walletbackend=sqlite"); | ||
| 2382 | vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); | ||
| 2383 | vEncryptArgs.push_back("-nogui"); | ||
| 2384 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2385 | nFail += Check(nEncryptRet == 0, | ||
| 2386 | "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; | ||
| 2387 | nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), | ||
| 2388 | "encryption took the SQLite persistence path") ? 0 : 1; | ||
| 2389 | nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), | ||
| 2390 | "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; | ||
| 2391 | |||
| 2392 | // The encrypted SQLite wallet must no longer hold the plaintext secrets. | ||
| 2393 | nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2394 | "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; | ||
| 2395 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), | ||
| 2396 | "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; | ||
| 2397 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), | ||
| 2398 | "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; | ||
| 2399 | |||
| 2400 | // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path | ||
| 2401 | // must never touch it, so its plaintext key is still exactly there. | ||
| 2402 | nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), | ||
| 2403 | "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; | ||
| 2404 | |||
| 2405 | // Step 3: a locked encrypted SQLite wallet reports that phrase coverage | ||
| 2406 | // needs an unlock, exactly as the Berkeley DB path does (exit code 2). | ||
| 2407 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2408 | vector<string> vLockedArgs; | ||
| 2409 | vLockedArgs.push_back("-datadir=" + tmp); | ||
| 2410 | vLockedArgs.push_back("-walletbackend=sqlite"); | ||
| 2411 | vLockedArgs.push_back("-recoveryaudit"); | ||
| 2412 | vLockedArgs.push_back("-nogui"); | ||
| 2413 | int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); | ||
| 2414 | nFail += Check(nLockedRet == 2 && | ||
| 2415 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), | ||
| 2416 | "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2417 | |||
| 2418 | // The wrong passphrase is rejected and reveals nothing. | ||
| 2419 | string strWrongDump = tmp + "/wrong-dump.txt"; | ||
| 2420 | vector<string> vWrongArgs; | ||
| 2421 | vWrongArgs.push_back("-datadir=" + tmp); | ||
| 2422 | vWrongArgs.push_back("-walletbackend=sqlite"); | ||
| 2423 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | ||
| 2424 | vWrongArgs.push_back("-dumpwallet=" + strWrongDump); | ||
| 2425 | vWrongArgs.push_back("-nogui"); | ||
| 2426 | int nWrongRet = RunBitflashChild(strExe, vWrongArgs); | ||
| 2427 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | ||
| 2428 | "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; | ||
| 2429 | |||
| 2430 | // The right passphrase unlocks and can dump the original key. | ||
| 2431 | string strRightDump = tmp + "/right-dump.txt"; | ||
| 2432 | vector<string> vRightArgs; | ||
| 2433 | vRightArgs.push_back("-datadir=" + tmp); | ||
| 2434 | vRightArgs.push_back("-walletbackend=sqlite"); | ||
| 2435 | vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2436 | vRightArgs.push_back("-dumpwallet=" + strRightDump); | ||
| 2437 | vRightArgs.push_back("-nogui"); | ||
| 2438 | int nRightRet = RunBitflashChild(strExe, vRightArgs); | ||
| 2439 | nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), | ||
| 2440 | "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; | ||
| 2441 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | ||
| 2442 | "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; | ||
| 2443 | |||
| 2444 | // The recovery phrase survives the encryption. | ||
| 2445 | string strAudit = tmp + "/recovery-audit.txt"; | ||
| 2446 | vector<string> vAuditArgs; | ||
| 2447 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2448 | vAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2449 | vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2450 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2451 | vAuditArgs.push_back("-nogui"); | ||
| 2452 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2453 | nFail += Check(nAuditRet == 0 && | ||
| 2454 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2455 | "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; | ||
| 2456 | |||
| 2457 | // One-shot storage commands honour -walletbackend. The SQLite wallet is | ||
| 2458 | // encrypted; the wallet.dat beside it is still plaintext. Same datadir, | ||
| 2459 | // opposite answers -> the flag routes the diagnostic to the right file. | ||
| 2460 | string strSqliteAuditJson = tmp + "/sqlite-audit.json"; | ||
| 2461 | vector<string> vSqliteAuditArgs; | ||
| 2462 | vSqliteAuditArgs.push_back("-datadir=" + tmp); | ||
| 2463 | vSqliteAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2464 | vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson); | ||
| 2465 | vSqliteAuditArgs.push_back("-nogui"); | ||
| 2466 | int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs); | ||
| 2467 | nFail += Check(nSqliteAuditRet == 0 && | ||
| 2468 | FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") && | ||
| 2469 | FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""), | ||
| 2470 | "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2471 | |||
| 2472 | string strBdbAuditJson = tmp + "/bdb-audit.json"; | ||
| 2473 | vector<string> vBdbAuditArgs; | ||
| 2474 | vBdbAuditArgs.push_back("-datadir=" + tmp); | ||
| 2475 | vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson); | ||
| 2476 | vBdbAuditArgs.push_back("-nogui"); | ||
| 2477 | int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs); | ||
| 2478 | nFail += Check(nBdbAuditRet == 0 && | ||
| 2479 | FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") && | ||
| 2480 | FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""), | ||
| 2481 | "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1; | ||
| 2482 | |||
| 2483 | string strSqliteCheckOut = tmp + "/sqlite-check.txt"; | ||
| 2484 | vector<string> vSqliteCheckArgs; | ||
| 2485 | vSqliteCheckArgs.push_back("-datadir=" + tmp); | ||
| 2486 | vSqliteCheckArgs.push_back("-walletbackend=sqlite"); | ||
| 2487 | vSqliteCheckArgs.push_back("-walletstoragecheck"); | ||
| 2488 | vSqliteCheckArgs.push_back("-nogui"); | ||
| 2489 | int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut); | ||
| 2490 | nFail += Check(nSqliteCheckRet == 0, | ||
| 2491 | "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2492 | |||
| 2493 | // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat. | ||
| 2494 | // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds | ||
| 2495 | // none of the plaintext key that wallet.dat still contains. | ||
| 2496 | string strBackupDest = tmp + "/backup.sqlite"; | ||
| 2497 | vector<string> vBackupArgs; | ||
| 2498 | vBackupArgs.push_back("-datadir=" + tmp); | ||
| 2499 | vBackupArgs.push_back("-walletbackend=sqlite"); | ||
| 2500 | vBackupArgs.push_back("-backupwallet=" + strBackupDest); | ||
| 2501 | vBackupArgs.push_back("-nogui"); | ||
| 2502 | int nBackupRet = RunBitflashChild(strExe, vBackupArgs); | ||
| 2503 | nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()), | ||
| 2504 | "-backupwallet under SQLite writes a backup file") ? 0 : 1; | ||
| 2505 | nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes), | ||
| 2506 | "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1; | ||
| 2507 | |||
| 2508 | string strBackupLoadCheck = tmp + "/backup-loadcheck.txt"; | ||
| 2509 | vector<string> vLoadCheckArgs; | ||
| 2510 | vLoadCheckArgs.push_back("-datadir=" + tmp); | ||
| 2511 | vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest); | ||
| 2512 | vLoadCheckArgs.push_back("-nogui"); | ||
| 2513 | int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck); | ||
| 2514 | nFail += Check(nBackupLoadRet == 0, | ||
| 2515 | "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1; | ||
| 2516 | } | ||
| 2517 | catch (const std::exception& e) | ||
| 2518 | { | ||
| 2519 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2520 | nFail++; | ||
| 2521 | } | ||
| 2522 | catch (...) | ||
| 2523 | { | ||
| 2524 | printf(" FAIL unknown exception\n"); | ||
| 2525 | nFail++; | ||
| 2526 | } | ||
| 2527 | |||
| 2528 | DBFlush(true); | ||
| 2529 | SetCurrentDir(cwd); | ||
| 2530 | RemoveTree(tmp); | ||
| 2531 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2532 | nFail, nFail == 1 ? "" : "s"); | ||
| 2533 | fflush(stdout); | ||
| 2534 | return nFail == 0 ? 0 : 1; | ||
| 2535 | } | ||
| 2536 | |||
| 2255 | static int RunWalletPortabilitySelfTest() | 2537 | static int RunWalletPortabilitySelfTest() |
| 2256 | { | 2538 | { |
| 2257 | fflush(stdout); | 2539 | fflush(stdout); |
| @@ -2981,6 +3263,8 @@ int RunSelfTest(const std::string& name) | |||
| 2981 | return RunWalletCryptoSelfTest(); | 3263 | return RunWalletCryptoSelfTest(); |
| 2982 | if (name == "wallet-encrypt") | 3264 | if (name == "wallet-encrypt") |
| 2983 | return RunWalletEncryptSelfTest(); | 3265 | return RunWalletEncryptSelfTest(); |
| 3266 | if (name == "wallet-sqlite-encrypt") | ||
| 3267 | return RunWalletSQLiteEncryptSelfTest(); | ||
| 2984 | if (name == "wallet-portability") | 3268 | if (name == "wallet-portability") |
| 2985 | return RunWalletPortabilitySelfTest(); | 3269 | return RunWalletPortabilitySelfTest(); |
| 2986 | if (name == "net-message") | 3270 | if (name == "net-message") |
| @@ -2997,6 +3281,6 @@ int RunSelfTest(const std::string& name) | |||
| 2997 | return RunManagedTorSelfTest(); | 3281 | return RunManagedTorSelfTest(); |
| 2998 | 3282 | ||
| 2999 | printf("Unknown self-test '%s'\n", name.c_str()); | 3283 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 3000 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); | 3284 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); |
| 3001 | return 1; | 3285 | return 1; |
| 3002 | } | 3286 | } |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 4dda8dd..374b2cc 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -1365,6 +1365,11 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1365 | { | 1365 | { |
| 1366 | AttachTerminal(); | 1366 | AttachTerminal(); |
| 1367 | 1367 | ||
| 1368 | // Capture the backend now: under SQLite, EncryptWallet() closes the runtime | ||
| 1369 | // store to swap the file, so WalletSQLiteRuntimeActive() reads false by the | ||
| 1370 | // time we would decide whether to purge Berkeley DB logs below. | ||
| 1371 | bool fSQLiteBackend = WalletSQLiteRuntimeActive(); | ||
| 1372 | |||
| 1368 | string strBackup; | 1373 | string strBackup; |
| 1369 | string strError; | 1374 | string strError; |
| 1370 | if (!EncryptWallet(strPassphrase, strBackup, strError)) | 1375 | if (!EncryptWallet(strPassphrase, strBackup, strError)) |
| @@ -1375,6 +1380,8 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1375 | } | 1380 | } |
| 1376 | 1381 | ||
| 1377 | printf("Wallet encrypted.\n"); | 1382 | printf("Wallet encrypted.\n"); |
| 1383 | if (fSQLiteBackend) | ||
| 1384 | printf("Wallet backend: SQLite (wallet.sqlite).\n"); | ||
| 1378 | printf("The old unencrypted wallet was moved to:\n"); | 1385 | printf("The old unencrypted wallet was moved to:\n"); |
| 1379 | printf(" %s\n", strBackup.c_str()); | 1386 | printf(" %s\n", strBackup.c_str()); |
| 1380 | printf("That backup still contains private keys in plain text. Move it offline or\n"); | 1387 | printf("That backup still contains private keys in plain text. Move it offline or\n"); |
| @@ -1387,7 +1394,13 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1387 | // them. Closing the environment and dropping the logs is the last step of | 1394 | // them. Closing the environment and dropping the logs is the last step of |
| 1388 | // encrypting, not an optimisation -- our own backup advice is to copy the | 1395 | // encrypting, not an optimisation -- our own backup advice is to copy the |
| 1389 | // whole directory, and following it otherwise carries the keys along. | 1396 | // whole directory, and following it otherwise carries the keys along. |
| 1390 | PurgeDbEnvironmentLogs(); | 1397 | // |
| 1398 | // The SQLite backend has no Berkeley DB environment behind the wallet, and | ||
| 1399 | // EncryptWallet() already closed and swapped the SQLite store; there is | ||
| 1400 | // nothing to purge, and calling into the environment here would only open | ||
| 1401 | // one needlessly. | ||
| 1402 | if (!fSQLiteBackend) | ||
| 1403 | PurgeDbEnvironmentLogs(); | ||
| 1391 | return 0; | 1404 | return 0; |
| 1392 | } | 1405 | } |
| 1393 | 1406 | ||