upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <admin@bitflash.network>2026-08-17 10:37:07 -0300
committerBitflash <admin@bitflash.network>2026-08-17 10:37:07 -0300
commitf7033201689c6eb70f16b242b9f90be548f67aa5 (patch)
treee6ecab8a276afb007f341a498dd1a8553a756b0c
parentc56af18e421dff78446399a630b5fa949fc487b3 (diff)
parentf9ee957bd6d3f28dfee166a82f389aad666a0119 (diff)
Merge pull request 'wallet: stage SQLite runtime loading' (#55) from wallet-sqlite-runtime into main
-rw-r--r--docs/storage.md4
-rw-r--r--src/db.cpp361
-rw-r--r--src/db.h1
-rw-r--r--src/main_gui.cpp22
-rw-r--r--src/selftest.cpp29
-rw-r--r--src/wallet_sqlite.cpp19
-rw-r--r--src/wallet_sqlite.h1
7 files changed, 420 insertions, 17 deletions
diff --git a/docs/storage.md b/docs/storage.md
index 07c7170..08c4a01 100644
--- a/docs/storage.md
+++ b/docs/storage.md
@@ -120,6 +120,10 @@ file and failed restores remove the incomplete `wallet.dat`.
120`-walletsqliteloadcheck=FILE` parses a SQLite export using the same record 120`-walletsqliteloadcheck=FILE` parses a SQLite export using the same record
121types the wallet loader understands, so a migration can prove the export is not 121types the wallet loader understands, so a migration can prove the export is not
122just byte-preserving but loader-compatible before runtime activation. 122just byte-preserving but loader-compatible before runtime activation.
123`-walletsqlite=FILE` is the first runtime staging flag: it loads a SQLite export
124through the startup wallet path, initializes the in-memory wallet state, reports
125success, and exits before network, mining, GUI, or wallet mutation begins. It is
126deliberately read-only until the write path is routed to SQLite as well.
123 127
124## `blkindex.dat` 128## `blkindex.dat`
125 129
diff --git a/src/db.cpp b/src/db.cpp
index 8b82fe5..581e5a7 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -3,6 +3,7 @@
3// file license.txt or http://www.opensource.org/licenses/mit-license.php. 3// file license.txt or http://www.opensource.org/licenses/mit-license.php.
4 4
5#include "headers.h" 5#include "headers.h"
6#include "wallet_sqlite.h"
6 7
7 8
8 9
@@ -650,6 +651,289 @@ bool CReviewDB::WriteReviews(uint256 hash, const vector<CReview>& vReviews)
650// CWalletDB 651// CWalletDB
651// 652//
652 653
654class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor
655{
656public:
657 bool fHaveStoredMineMode;
658 bool fHaveStoredHDCoinType;
659 string strLastType;
660
661 CSQLiteWalletRuntimeLoadVisitor() :
662 fHaveStoredMineMode(false),
663 fHaveStoredHDCoinType(false)
664 {
665 }
666
667 bool VisitWalletRecord(const CDataStream& ssKeyIn,
668 const CDataStream& ssValueIn,
669 string& strErrorRet)
670 {
671 CDataStream ssKey = ssKeyIn;
672 CDataStream ssValue = ssValueIn;
673 string strType;
674 ssKey >> strType;
675 strLastType = strType;
676
677 if (strType == "name")
678 {
679 string strAddress;
680 ssKey >> strAddress;
681 ssValue >> mapAddressBook[strAddress];
682 }
683 else if (strType == "tx")
684 {
685 uint256 hash;
686 ssKey >> hash;
687 CWalletTx& wtx = mapWallet[hash];
688 ssValue >> wtx;
689
690 if (wtx.GetHash() != hash)
691 printf("Error in SQLite wallet export, hash mismatch\n");
692 }
693 else if (strType == "key")
694 {
695 vector<unsigned char> vchPubKey;
696 ssKey >> vchPubKey;
697 CPrivKey vchPrivKey;
698 ssValue >> vchPrivKey;
699
700 mapKeys[vchPubKey] = vchPrivKey;
701 mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
702 }
703 else if (strType == "mkey")
704 {
705 unsigned int nID = 0;
706 ssKey >> nID;
707 CWalletMasterKey kMasterKey;
708 ssValue >> kMasterKey;
709 mapMasterKeys[nID] = kMasterKey;
710 nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID);
711 fWalletEncrypted = true;
712 fWalletLocked = true;
713 }
714 else if (strType == "ckey")
715 {
716 vector<unsigned char> vchPubKey;
717 ssKey >> vchPubKey;
718 vector<unsigned char> vchCryptedSecret;
719 ssValue >> vchCryptedSecret;
720 if (!AddCryptedKey(vchPubKey, vchCryptedSecret))
721 {
722 strErrorRet = "SQLite wallet export has an unreadable encrypted key record";
723 return false;
724 }
725 }
726 else if (strType == "defaultkey")
727 {
728 ssValue >> vchDefaultKey;
729 }
730 else if (strType == "hdmaster")
731 {
732 ssValue >> vchHDMaster;
733 }
734 else if (strType == "hdchaincode")
735 {
736 ssValue >> vchHDChainCode;
737 }
738 else if (strType == "hdnext")
739 {
740 ssValue >> nHDNext;
741 }
742 else if (strType == "hdschema")
743 {
744 ssValue >> nHDKeySchema;
745 }
746 else if (strType == "hdcointype")
747 {
748 ssValue >> nHDCoinType;
749 fHaveStoredHDCoinType = true;
750 }
751 else if (strType == "hdreceivenext")
752 {
753 ssValue >> nHDReceiveNext;
754 }
755 else if (strType == "hdchangenext")
756 {
757 ssValue >> nHDChangeNext;
758 }
759 else if (strType == "cryptedhdmaster")
760 {
761 ssValue >> vchCryptedHDMaster;
762 fWalletEncrypted = true;
763 fWalletLocked = true;
764 }
765 else if (strType == "cryptedhdchaincode")
766 {
767 ssValue >> vchCryptedHDChainCode;
768 fWalletEncrypted = true;
769 fWalletLocked = true;
770 }
771 else if (strType == "walletminversion")
772 {
773 int nMinVersion = 0;
774 ssValue >> nMinVersion;
775 if (nMinVersion > WALLET_FORMAT_SUPPORTED)
776 {
777 strWalletLoadError = strprintf(
778 "This SQLite wallet export was written by a newer version of "
779 "Bitflash (wallet format %d; this build understands %d). "
780 "Upgrade Bitflash before opening it.",
781 nMinVersion, WALLET_FORMAT_SUPPORTED);
782 strErrorRet = strWalletLoadError;
783 return false;
784 }
785 }
786 else if (strType == "pool")
787 {
788 int64 nIndex;
789 ssKey >> nIndex;
790 vector<unsigned char> vchPubKey;
791 ssValue >> vchPubKey;
792 mapKeyPool[nIndex] = vchPubKey;
793 }
794 else if (strType == "setting")
795 {
796 string strKey;
797 ssKey >> strKey;
798 if (strKey == "nTransactionFee") ssValue >> nTransactionFee;
799 if (strKey == "addrIncoming") ssValue >> addrIncoming;
800
801 if (!fMineModeFromCommandLine)
802 {
803 if (strKey == "nMineMode") { ssValue >> nMineMode; fHaveStoredMineMode = true; }
804 if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins;
805 if (strKey == "strParticipantPool") ssValue >> strParticipantPool;
806 }
807 }
808
809 if (ssKey.fail())
810 {
811 strErrorRet = strprintf("malformed SQLite wallet record key while handling '%s'",
812 strLastType.c_str());
813 return false;
814 }
815 if (ssValue.fail())
816 {
817 strErrorRet = strprintf("malformed SQLite wallet record value while handling '%s'",
818 strLastType.c_str());
819 return false;
820 }
821 return true;
822 }
823
824 vector<unsigned char> vchDefaultKey;
825};
826
827static bool FinishLoadedWallet(const vector<unsigned char>& vchDefaultKey,
828 bool fMayCreateDefaultKey)
829{
830 if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey)))
831 {
832 keyUser.SetPubKey(vchDefaultKey);
833 if (mapKeys.count(vchDefaultKey))
834 keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
835 }
836 else if (IsWalletEncrypted())
837 {
838 printf("LoadWallet: encrypted wallet has no usable default public key. "
839 "The file was left untouched.\n");
840 return false;
841 }
842 else
843 {
844 if (!fMayCreateDefaultKey)
845 {
846 strWalletLoadError =
847 "SQLite wallet export has no usable default public key";
848 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
849 return false;
850 }
851
852 keyUser.MakeNewKey();
853 if (!AddKey(keyUser))
854 return false;
855 if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
856 return false;
857 CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
858 }
859
860 return true;
861}
862
863static bool ReconcileLoadedWalletMetadata(bool fHaveStoredMineMode,
864 bool fHaveStoredHDCoinType)
865{
866 // An encrypted wallet has a seed; it just has not been decrypted yet.
867 //
868 // The reset in the else branch was written when "no seed in memory after
869 // loading" could only mean "this wallet has no seed". With encryption that
870 // is no longer true, and taking the else branch here wiped the schema and
871 // the derivation counters that had just been read correctly out of the
872 // file. Unlocking later restores the seed but nothing restores those, so a
873 // BIP44 wallet came back deriving legacy addresses -- different addresses
874 // from the ones its own recovery phrase produces, while the audit reported
875 // the phrase as covering nothing.
876 if (HaveHDSeed() || !vchCryptedHDMaster.empty())
877 {
878 if (nHDKeySchema == HD_SCHEMA_NONE)
879 {
880 nHDKeySchema = HD_SCHEMA_LEGACY;
881 printf("LoadWallet: deterministic seed has no schema metadata; "
882 "treating it as legacy m/index'\n");
883 }
884 else if (nHDKeySchema != HD_SCHEMA_LEGACY &&
885 nHDKeySchema != HD_SCHEMA_BIP44)
886 {
887 printf("LoadWallet: deterministic wallet schema %d (%s) is not "
888 "supported by this build\n",
889 nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str());
890 return false;
891 }
892 if (!fHaveStoredHDCoinType)
893 {
894 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
895 printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; "
896 "using Bitflash coin type %u\n", nHDCoinType);
897 }
898 }
899 else
900 {
901 nHDKeySchema = HD_SCHEMA_NONE;
902 nHDNext = 0;
903 nHDReceiveNext = 0;
904 nHDChangeNext = 0;
905 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
906 }
907
908 // fGenerateBitcoins and nMineMode only mean anything together, and a
909 // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already
910 // wrote fGenerateBitcoins when mining was toggled, years before this fork
911 // had a mode at all. Restoring that lone flag put a node into the one state
912 // the miner cannot act on -- generate on, mode relay -- so every miner
913 // thread started and returned at the relay guard, and the machine sat there
914 // looking like it was mining while hashing nothing. That happened to a
915 // 32-core node on the first 1.2.11 start.
916 //
917 // So the flag is only believed when the mode it belongs to was stored with
918 // it, and the pair is made coherent either way.
919 if (!fMineModeFromCommandLine)
920 {
921 if (!fHaveStoredMineMode)
922 {
923 if (fGenerateBitcoins)
924 printf("LoadWallet: ignoring a stored fGenerateBitcoins with no mining mode "
925 "beside it -- it predates this setting and cannot be read on its own\n");
926 fGenerateBitcoins = 0;
927 }
928 else
929 {
930 fGenerateBitcoins = (nMineMode != MINE_RELAY) ? 1 : 0;
931 }
932 }
933
934 return true;
935}
936
653bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) 937bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
654{ 938{
655 // Whether wallet.dat actually carried a mining mode, as opposed to just the 939 // Whether wallet.dat actually carried a mining mode, as opposed to just the
@@ -1005,31 +1289,74 @@ bool LoadWallet()
1005 if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) 1289 if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
1006 return false; 1290 return false;
1007 1291
1008 if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) 1292 return FinishLoadedWallet(vchDefaultKey, true);
1293}
1294
1295bool LoadWalletFromSQLite(const string& strPath)
1296{
1297 strWalletLoadError.clear();
1298
1299 if (!FileExists(strPath.c_str()))
1009 { 1300 {
1010 // Set keyUser 1301 strWalletLoadError = strprintf("SQLite wallet export does not exist: %s",
1011 keyUser.SetPubKey(vchDefaultKey); 1302 strPath.c_str());
1012 if (mapKeys.count(vchDefaultKey)) 1303 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1013 keyUser.SetPrivKey(mapKeys[vchDefaultKey]); 1304 return false;
1014 } 1305 }
1015 else if (IsWalletEncrypted()) 1306
1307 CWalletDBSQLite sqlite;
1308 string strError;
1309 if (!sqlite.OpenReadOnly(strPath, strError))
1016 { 1310 {
1017 printf("LoadWallet: encrypted wallet has no usable default public key. " 1311 strWalletLoadError = strError;
1018 "The file was left untouched.\n"); 1312 printf("LoadWallet: cannot open SQLite wallet export: %s\n",
1313 strError.c_str());
1019 return false; 1314 return false;
1020 } 1315 }
1021 else 1316
1317 CSQLiteWalletRuntimeLoadVisitor visitor;
1318 try
1022 { 1319 {
1023 // Create new keyUser and set as default key 1320 CRITICAL_BLOCK(cs_mapKeys)
1024 keyUser.MakeNewKey(); 1321 CRITICAL_BLOCK(cs_mapWallet)
1025 if (!AddKey(keyUser)) 1322 {
1026 return false; 1323 if (!sqlite.ScanRecords(visitor, strError))
1027 if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) 1324 {
1028 return false; 1325 if (strWalletLoadError.empty())
1029 CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); 1326 strWalletLoadError = strError;
1327 printf("LoadWallet: SQLite wallet export could not be read. "
1328 "Failed while handling a '%s' record: %s\n",
1329 visitor.strLastType.c_str(), strError.c_str());
1330 return false;
1331 }
1332 }
1333 }
1334 catch (const std::exception& e)
1335 {
1336 strWalletLoadError = strprintf(
1337 "SQLite wallet export could not be read while handling a '%s' record: %s",
1338 visitor.strLastType.c_str(), e.what());
1339 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1340 return false;
1341 }
1342 catch (...)
1343 {
1344 strWalletLoadError = strprintf(
1345 "SQLite wallet export could not be read while handling a '%s' record",
1346 visitor.strLastType.c_str());
1347 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1348 return false;
1030 } 1349 }
1031 1350
1032 return true; 1351 if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode,
1352 visitor.fHaveStoredHDCoinType))
1353 {
1354 if (strWalletLoadError.empty())
1355 strWalletLoadError = "SQLite wallet metadata is not supported by this build";
1356 return false;
1357 }
1358
1359 return FinishLoadedWallet(visitor.vchDefaultKey, false);
1033} 1360}
1034 1361
1035 1362
diff --git a/src/db.h b/src/db.h
index 53bda59..59ff24a 100644
--- a/src/db.h
+++ b/src/db.h
@@ -536,6 +536,7 @@ public:
536bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet); 536bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet);
537 537
538bool LoadWallet(); 538bool LoadWallet();
539bool LoadWalletFromSQLite(const string& strPath);
539 540
540// Writes a wallet.dat that opens on its own, anywhere -- see the definition in 541// Writes a wallet.dat that opens on its own, anywhere -- see the definition in
541// db.cpp for why a plain file copy does not. Still a point-in-time snapshot: 542// db.cpp for why a plain file copy does not. Still a point-in-time snapshot:
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index c9aac7d..ee875c7 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -189,6 +189,7 @@ static void PrintUsage()
189 printf(" an empty data directory, then exit)\n"); 189 printf(" an empty data directory, then exit)\n");
190 printf(" /walletsqliteloadcheck=FILE\n"); 190 printf(" /walletsqliteloadcheck=FILE\n");
191 printf(" (parse a SQLite export like the wallet loader)\n"); 191 printf(" (parse a SQLite export like the wallet loader)\n");
192 printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n");
192 printf(" /rescan (walk the chain for coins this wallet owns\n"); 193 printf(" /rescan (walk the chain for coins this wallet owns\n");
193 printf(" but never recorded, then exit)\n"); 194 printf(" but never recorded, then exit)\n");
194 printf("\n"); 195 printf("\n");
@@ -617,6 +618,27 @@ int main(int argc, char* argv[])
617 printf("Loading wallet...\n"); 618 printf("Loading wallet...\n");
618 try 619 try
619 { 620 {
621 string strWalletSQLite =
622 argval2(argc, argv, "/walletsqlite", "-walletsqlite");
623 if (!strWalletSQLite.empty())
624 {
625 AttachTerminal();
626 if (!LoadWalletFromSQLite(strWalletSQLite))
627 {
628 FatalStartupError(fHeadlessStartup,
629 "Cannot open SQLite wallet export.",
630 strWalletLoadError);
631 return 1;
632 }
633
634 fprintf(stderr, "SQLite wallet loaded: read-only staging backend\n");
635 fprintf(stderr, "SQLite runtime writes are not enabled yet; exiting before "
636 "network, mining, GUI, or wallet mutation starts.\n");
637 fflush(stderr);
638 DBFlush(true);
639 return 0;
640 }
641
620 if (!LoadWallet()) 642 if (!LoadWallet())
621 { 643 {
622 // LoadWallet() explains itself into strWalletLoadError when it 644 // LoadWallet() explains itself into strWalletLoadError when it
diff --git a/src/selftest.cpp b/src/selftest.cpp
index feb4076..14bf45a 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -1298,6 +1298,19 @@ static int RunWalletSQLiteMigrationSelfTest()
1298 "load check: ok"), 1298 "load check: ok"),
1299 "SQLite wallet export passes the loader compatibility check") ? 0 : 1; 1299 "SQLite wallet export passes the loader compatibility check") ? 0 : 1;
1300 1300
1301 string strRuntimeOut = tmp + "/sqlite-runtime.txt";
1302 vector<string> vRuntimeArgs;
1303 vRuntimeArgs.push_back("-datadir=" + strWalletDir);
1304 vRuntimeArgs.push_back("-nomanagedtor");
1305 vRuntimeArgs.push_back("-nogui");
1306 vRuntimeArgs.push_back("-walletsqlite=" + strSQLite);
1307 int nRuntimeRet =
1308 RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut);
1309 nFail += Check(nRuntimeRet == 0 &&
1310 FileContainsText(strRuntimeOut,
1311 "SQLite wallet loaded: read-only staging backend"),
1312 "SQLite wallet export loads through the runtime startup path") ? 0 : 1;
1313
1301 string strRestoredDir = tmp + "/restored-wallet"; 1314 string strRestoredDir = tmp + "/restored-wallet";
1302 nFail += Check(MakeDirLocal(strRestoredDir), 1315 nFail += Check(MakeDirLocal(strRestoredDir),
1303 "restored wallet directory can be created") ? 0 : 1; 1316 "restored wallet directory can be created") ? 0 : 1;
@@ -1372,6 +1385,22 @@ static int RunWalletSQLiteMigrationSelfTest()
1372 "load check: failed"), 1385 "load check: failed"),
1373 "SQLite wallet load check rejects future wallet formats") ? 0 : 1; 1386 "SQLite wallet load check rejects future wallet formats") ? 0 : 1;
1374 1387
1388 string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt";
1389 vector<string> vFutureRuntimeArgs;
1390 vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir);
1391 vFutureRuntimeArgs.push_back("-nomanagedtor");
1392 vFutureRuntimeArgs.push_back("-nogui");
1393 vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite);
1394 int nFutureRuntimeRet =
1395 RunBitflashChild(strExe, vFutureRuntimeArgs, NULL,
1396 &strFutureRuntimeOut);
1397 nFail += Check(nFutureRuntimeRet == 1 &&
1398 FileContainsText(strFutureRuntimeOut,
1399 "Cannot open SQLite wallet export") &&
1400 FileContainsText(strFutureRuntimeOut,
1401 "newer version of Bitflash"),
1402 "SQLite runtime startup rejects future wallet formats") ? 0 : 1;
1403
1375 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; 1404 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt";
1376 int nOverwriteRet = 1405 int nOverwriteRet =
1377 RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); 1406 RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut);
diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp
index 8489cac..ca81573 100644
--- a/src/wallet_sqlite.cpp
+++ b/src/wallet_sqlite.cpp
@@ -58,6 +58,25 @@ bool CWalletDBSQLite::Open(const string& strPath, string& strError)
58 return true; 58 return true;
59} 59}
60 60
61bool CWalletDBSQLite::OpenReadOnly(const string& strPath, string& strError)
62{
63 Close();
64
65 int ret = sqlite3_open_v2(strPath.c_str(), &pdb,
66 SQLITE_OPEN_READONLY |
67 SQLITE_OPEN_FULLMUTEX,
68 NULL);
69 if (ret != SQLITE_OK)
70 {
71 SetSQLiteError(pdb, "cannot open SQLite wallet read-only", strError);
72 Close();
73 return false;
74 }
75
76 sqlite3_busy_timeout(pdb, 5000);
77 return true;
78}
79
61void CWalletDBSQLite::Close() 80void CWalletDBSQLite::Close()
62{ 81{
63 if (pdb) 82 if (pdb)
diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h
index 385a340..5e6467b 100644
--- a/src/wallet_sqlite.h
+++ b/src/wallet_sqlite.h
@@ -15,6 +15,7 @@ public:
15 ~CWalletDBSQLite(); 15 ~CWalletDBSQLite();
16 16
17 bool Open(const string& strPath, string& strError); 17 bool Open(const string& strPath, string& strError);
18 bool OpenReadOnly(const string& strPath, string& strError);
18 void Close(); 19 void Close();
19 20
20 bool BeginTransaction(string& strError); 21 bool BeginTransaction(string& strError);