diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/selftest.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff) | |
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we
build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped
line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git
merge-base returns no result at all -- so anyone cloning this repository
landed on a default branch that was neither current nor related to the
running node.
The 28 commits on main were audited before this merge: every one is already
present in the tree by content, including the two that are hardest to see
because they were rewritten afterwards -- the payout reorg-safety work
(PayoutBlockStillOnChain, since evolved to re-check on every retry rather
than once at maturity) and the nostr relay certificate verification
(SSL_set1_host, now with a Windows root-store path). Nothing on main is lost
by this merge.
Recorded as a merge rather than a force-push: main only ever moves forward,
its old history stays reachable as the first parent, and this is revertible.
The tree is taken wholesale from the merged branch, so main's content is now
exactly what was built and tested.
What this brings onto main:
- compactblock: BIP152-style compact block relay ported from upstream 1.2.23,
primitives and unit test only, with the RAND_bytes failure path closed and
the reconstruction allocation bounded.
- upstream/main 17741d9: isolated testnet parameters and genesis, optional
2 MB large pages for RandomX. Mainnet genesis verified byte-identical
through upstream's BuildGenesisBlock() refactor.
- deploy: every image built on bookworm so binaries run on Debian 12, and the
container build now honours the RandomX and libsecp256k1 pins it had been
silently ignoring since before 1.2.15.
Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus
constants unchanged against e4f898c apart from the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/selftest.cpp')
| -rw-r--r-- | src/selftest.cpp | 2611 |
1 files changed, 2594 insertions, 17 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp index ff0289b..357d532 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -6,9 +6,20 @@ | |||
| 6 | // and against a temporary data directory. | 6 | // and against a temporary data directory. |
| 7 | 7 | ||
| 8 | #include "headers_core.h" | 8 | #include "headers_core.h" |
| 9 | #include "btfaddr.h" | ||
| 9 | #include "bip32.h" | 10 | #include "bip32.h" |
| 11 | #include "proxy.h" | ||
| 10 | #include "selftest.h" | 12 | #include "selftest.h" |
| 13 | #ifndef _WIN32 | ||
| 14 | #include <sys/wait.h> // WIFEXITED/WEXITSTATUS for the child-process helper | ||
| 15 | #endif | ||
| 16 | #include "sockcount.h" | ||
| 17 | #include "tor.h" | ||
| 11 | #include "walletcmd.h" | 18 | #include "walletcmd.h" |
| 19 | #include "wallet_sqlite.h" | ||
| 20 | |||
| 21 | extern int RunPoolStratumSelfTest(); | ||
| 22 | extern int RunPayoutLedgerSelfTest(); | ||
| 12 | 23 | ||
| 13 | // Test results go to the terminal, not to debug.log. | 24 | // Test results go to the terminal, not to debug.log. |
| 14 | // | 25 | // |
| @@ -28,6 +39,7 @@ | |||
| 28 | #undef printf | 39 | #undef printf |
| 29 | 40 | ||
| 30 | #include <mutex> | 41 | #include <mutex> |
| 42 | #include <cerrno> | ||
| 31 | #include <stdexcept> | 43 | #include <stdexcept> |
| 32 | #include <thread> | 44 | #include <thread> |
| 33 | 45 | ||
| @@ -55,6 +67,11 @@ static std::string HexStrLocal(const std::vector<unsigned char>& v) | |||
| 55 | return out; | 67 | return out; |
| 56 | } | 68 | } |
| 57 | 69 | ||
| 70 | static vector<unsigned char> DataStreamBytes(const CDataStream& ss) | ||
| 71 | { | ||
| 72 | return vector<unsigned char>(ss.begin(), ss.end()); | ||
| 73 | } | ||
| 74 | |||
| 58 | static bool MakeTempDir(std::string& out) | 75 | static bool MakeTempDir(std::string& out) |
| 59 | { | 76 | { |
| 60 | #ifdef _WIN32 | 77 | #ifdef _WIN32 |
| @@ -348,17 +365,17 @@ static int RunWalletHDSelfTest() | |||
| 348 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; | 365 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; |
| 349 | nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, | 366 | nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, |
| 350 | "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; | 367 | "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; |
| 351 | nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | 368 | nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH, |
| 352 | "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; | 369 | "a new recovery phrase records the SLIP-0044 BIP44 coin type") ? 0 : 1; |
| 353 | nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, | 370 | nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, |
| 354 | "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; | 371 | "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; |
| 355 | std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | 372 | std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, |
| 356 | HD_BIP44_ACCOUNT, | 373 | HD_BIP44_ACCOUNT, |
| 357 | HD_BIP44_CHAIN_RECEIVE, | 374 | HD_BIP44_CHAIN_RECEIVE, |
| 358 | 0); | 375 | 0); |
| 359 | nFail += Check(vBIP44Path.size() == 5 && | 376 | nFail += Check(vBIP44Path.size() == 5 && |
| 360 | vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && | 377 | vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && |
| 361 | vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && | 378 | vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH | bitflash::BIP32_HARDENED) && |
| 362 | vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && | 379 | vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && |
| 363 | vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && | 380 | vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && |
| 364 | vBIP44Path[4] == 0, | 381 | vBIP44Path[4] == 0, |
| @@ -380,7 +397,7 @@ static int RunWalletHDSelfTest() | |||
| 380 | hdParent.chainCode = vchHDChainCode; | 397 | hdParent.chainCode = vchHDChainCode; |
| 381 | bitflash::BIP32PrivateNode hdBIP44ReceiveChild; | 398 | bitflash::BIP32PrivateNode hdBIP44ReceiveChild; |
| 382 | if (!bitflash::BIP32DerivePath(hdParent, | 399 | if (!bitflash::BIP32DerivePath(hdParent, |
| 383 | HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | 400 | HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, |
| 384 | HD_BIP44_ACCOUNT, | 401 | HD_BIP44_ACCOUNT, |
| 385 | HD_BIP44_CHAIN_RECEIVE, | 402 | HD_BIP44_CHAIN_RECEIVE, |
| 386 | 0), | 403 | 0), |
| @@ -568,7 +585,7 @@ static int RunWalletHDSelfTest() | |||
| 568 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; | 585 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; |
| 569 | nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, | 586 | nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, |
| 570 | "the recovery audit reports the derivation schema") ? 0 : 1; | 587 | "the recovery audit reports the derivation schema") ? 0 : 1; |
| 571 | nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | 588 | nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH, |
| 572 | "the recovery audit reports the BIP44 coin type") ? 0 : 1; | 589 | "the recovery audit reports the BIP44 coin type") ? 0 : 1; |
| 573 | nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, | 590 | nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, |
| 574 | "the recovery audit reports receive/change counters") ? 0 : 1; | 591 | "the recovery audit reports receive/change counters") ? 0 : 1; |
| @@ -582,6 +599,34 @@ static int RunWalletHDSelfTest() | |||
| 582 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; | 599 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; |
| 583 | nFail += Check(audit.nLegacyImmatureTx == 1, | 600 | nFail += Check(audit.nLegacyImmatureTx == 1, |
| 584 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; | 601 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; |
| 602 | |||
| 603 | CKey keyLegacyHD11; | ||
| 604 | nSchemaForLegacyAudit = nHDKeySchema; | ||
| 605 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 606 | if (!DeriveHDKey(11, keyLegacyHD11, strError)) | ||
| 607 | throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError); | ||
| 608 | nHDKeySchema = nSchemaForLegacyAudit; | ||
| 609 | if (!AddKey(keyLegacyHD11)) | ||
| 610 | throw std::runtime_error("could not store the legacy compatibility index 11 key"); | ||
| 611 | |||
| 612 | std::map<unsigned int, std::vector<unsigned char> > mapLegacyProbe; | ||
| 613 | mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey(); | ||
| 614 | mapLegacyProbe[11] = keyLegacyHD11.GetPubKey(); | ||
| 615 | |||
| 616 | CWalletTx wtxLegacyHD11; | ||
| 617 | wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG)); | ||
| 618 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 619 | { | ||
| 620 | mapWallet.clear(); | ||
| 621 | mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11; | ||
| 622 | } | ||
| 623 | nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12, | ||
| 624 | "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1; | ||
| 625 | |||
| 626 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 627 | mapWallet.clear(); | ||
| 628 | nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0, | ||
| 629 | "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1; | ||
| 585 | } | 630 | } |
| 586 | catch (const std::exception& e) | 631 | catch (const std::exception& e) |
| 587 | { | 632 | { |
| @@ -701,6 +746,1017 @@ static int RunWalletFormatSelfTest() | |||
| 701 | return nFail == 0 ? 0 : 1; | 746 | return nFail == 0 ? 0 : 1; |
| 702 | } | 747 | } |
| 703 | 748 | ||
| 749 | static bool FileContainsText(const string& strPath, const string& strNeedle); | ||
| 750 | static bool MakeDirLocal(const string& strPath); | ||
| 751 | static int RunBitflashChild(const string& strExe, | ||
| 752 | const vector<string>& vArgs, | ||
| 753 | const string* pStdinFile, | ||
| 754 | const string* pOutputFile); | ||
| 755 | |||
| 756 | class CWalletStorageSanityRawDB : public CWalletDB | ||
| 757 | { | ||
| 758 | public: | ||
| 759 | CWalletStorageSanityRawDB(const char* pszMode="r+") | ||
| 760 | : CWalletDB(pszMode) { } | ||
| 761 | |||
| 762 | bool WriteUnknownRecord() | ||
| 763 | { | ||
| 764 | return Write(string("storage-sanity-unknown"), 1); | ||
| 765 | } | ||
| 766 | |||
| 767 | bool WriteMalformedKeyRecord() | ||
| 768 | { | ||
| 769 | if (!pdb) | ||
| 770 | return false; | ||
| 771 | unsigned char chKey = 0xff; | ||
| 772 | unsigned char chValue = 0x01; | ||
| 773 | Dbt datKey(&chKey, 1); | ||
| 774 | Dbt datValue(&chValue, 1); | ||
| 775 | return pdb->put(GetTxn(), &datKey, &datValue, 0) == 0; | ||
| 776 | } | ||
| 777 | |||
| 778 | bool MakePlainHDSeedIncomplete() | ||
| 779 | { | ||
| 780 | return Erase(string("hdchaincode")); | ||
| 781 | } | ||
| 782 | |||
| 783 | bool AddEncryptedMarkerToPlainWallet() | ||
| 784 | { | ||
| 785 | CWalletMasterKey kMasterKey; | ||
| 786 | kMasterKey.vchCryptedKey.resize(48); | ||
| 787 | for (size_t i = 0; i < kMasterKey.vchCryptedKey.size(); i++) | ||
| 788 | kMasterKey.vchCryptedKey[i] = (unsigned char)(0x80 + (i & 0x3f)); | ||
| 789 | for (size_t i = 0; i < kMasterKey.vchSalt.size(); i++) | ||
| 790 | kMasterKey.vchSalt[i] = (unsigned char)(0x40 + i); | ||
| 791 | return WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) && | ||
| 792 | WriteMasterKey(1, kMasterKey); | ||
| 793 | } | ||
| 794 | }; | ||
| 795 | |||
| 796 | static const char* SELFTEST_MUTATE_WALLET_MARKER = | ||
| 797 | ".bitflash-selftest-wallet-mutate-ok"; | ||
| 798 | static const char* SELFTEST_MUTATE_WALLET_MARKER_TEXT = | ||
| 799 | "bitflash destructive wallet storage self-test\n"; | ||
| 800 | |||
| 801 | static string SelfTestMutationMarkerPath() | ||
| 802 | { | ||
| 803 | return GetAppDir() + "/" + SELFTEST_MUTATE_WALLET_MARKER; | ||
| 804 | } | ||
| 805 | |||
| 806 | static bool WriteSelfTestMutationMarker(const string& strDir) | ||
| 807 | { | ||
| 808 | string strPath = strDir + "/" + SELFTEST_MUTATE_WALLET_MARKER; | ||
| 809 | FILE* pf = fopen(strPath.c_str(), "wb"); | ||
| 810 | if (!pf) | ||
| 811 | return false; | ||
| 812 | bool fOk = fwrite(SELFTEST_MUTATE_WALLET_MARKER_TEXT, 1, | ||
| 813 | strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT), pf) == | ||
| 814 | strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT); | ||
| 815 | if (fclose(pf) != 0) | ||
| 816 | fOk = false; | ||
| 817 | return fOk; | ||
| 818 | } | ||
| 819 | |||
| 820 | int RunSelfTestMutateWallet(const std::string& name) | ||
| 821 | { | ||
| 822 | if (!FileContainsText(SelfTestMutationMarkerPath(), | ||
| 823 | SELFTEST_MUTATE_WALLET_MARKER_TEXT)) | ||
| 824 | { | ||
| 825 | fprintf(stderr, "Refusing destructive wallet storage mutation outside " | ||
| 826 | "a marked self-test datadir.\n"); | ||
| 827 | return 1; | ||
| 828 | } | ||
| 829 | |||
| 830 | bool fOk = false; | ||
| 831 | try | ||
| 832 | { | ||
| 833 | CWalletStorageSanityRawDB walletdb; | ||
| 834 | if (name == "unknown-record") | ||
| 835 | fOk = walletdb.WriteUnknownRecord(); | ||
| 836 | else if (name == "malformed-key") | ||
| 837 | fOk = walletdb.WriteMalformedKeyRecord(); | ||
| 838 | else if (name == "incomplete-plain-hd") | ||
| 839 | fOk = walletdb.MakePlainHDSeedIncomplete(); | ||
| 840 | else if (name == "encrypted-marker-with-plain-keys") | ||
| 841 | fOk = walletdb.AddEncryptedMarkerToPlainWallet(); | ||
| 842 | else | ||
| 843 | { | ||
| 844 | fprintf(stderr, "Unknown wallet storage mutation '%s'\n", name.c_str()); | ||
| 845 | return 1; | ||
| 846 | } | ||
| 847 | } | ||
| 848 | catch (const std::exception& e) | ||
| 849 | { | ||
| 850 | fprintf(stderr, "Cannot mutate wallet storage: %s\n", e.what()); | ||
| 851 | return 1; | ||
| 852 | } | ||
| 853 | catch (...) | ||
| 854 | { | ||
| 855 | fprintf(stderr, "Cannot mutate wallet storage: unknown exception\n"); | ||
| 856 | return 1; | ||
| 857 | } | ||
| 858 | |||
| 859 | DBFlush(true); | ||
| 860 | return fOk ? 0 : 1; | ||
| 861 | } | ||
| 862 | |||
| 863 | static int RunWalletStorageSanitySelfTest() | ||
| 864 | { | ||
| 865 | fflush(stdout); | ||
| 866 | printf("wallet-storage-sanity self-test\n"); | ||
| 867 | |||
| 868 | std::string tmp; | ||
| 869 | std::string cwd; | ||
| 870 | if (!MakeTempDir(tmp)) | ||
| 871 | { | ||
| 872 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 873 | return 1; | ||
| 874 | } | ||
| 875 | if (!GetCurrentDir(cwd)) | ||
| 876 | { | ||
| 877 | printf(" FAIL could not read current directory\n"); | ||
| 878 | RemoveTree(tmp); | ||
| 879 | return 1; | ||
| 880 | } | ||
| 881 | |||
| 882 | int nFail = 0; | ||
| 883 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 884 | |||
| 885 | try | ||
| 886 | { | ||
| 887 | #ifdef _WIN32 | ||
| 888 | string strExe = cwd + "\\bitflash.exe"; | ||
| 889 | #else | ||
| 890 | string strExe = cwd + "/bitflash-node"; | ||
| 891 | #endif | ||
| 892 | |||
| 893 | struct StorageScenario | ||
| 894 | { | ||
| 895 | const char* pszName; | ||
| 896 | const char* pszExpectedFailure; | ||
| 897 | }; | ||
| 898 | |||
| 899 | StorageScenario scenarios[] = { | ||
| 900 | { "unknown-record", "wallet.dat contains unknown records" }, | ||
| 901 | { "malformed-key", "wallet.dat contains malformed records" }, | ||
| 902 | { "incomplete-plain-hd", "plain HD seed is incomplete" }, | ||
| 903 | { "encrypted-marker-with-plain-keys", | ||
| 904 | "encrypted wallet still contains plain private key records" }, | ||
| 905 | }; | ||
| 906 | |||
| 907 | for (size_t i = 0; i < sizeof(scenarios)/sizeof(scenarios[0]); i++) | ||
| 908 | { | ||
| 909 | string strScenarioDir = tmp + "/" + scenarios[i].pszName; | ||
| 910 | nFail += Check(MakeDirLocal(strScenarioDir), | ||
| 911 | "scenario datadir can be created") ? 0 : 1; | ||
| 912 | |||
| 913 | string strCreateOut = tmp + "/" + scenarios[i].pszName + "-create.txt"; | ||
| 914 | vector<string> vCreateArgs; | ||
| 915 | vCreateArgs.push_back("-datadir=" + strScenarioDir); | ||
| 916 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 917 | vCreateArgs.push_back("-nogui"); | ||
| 918 | vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat | ||
| 919 | vCreateArgs.push_back("-newphrase"); | ||
| 920 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 921 | nFail += Check(nCreateRet == 0 && | ||
| 922 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 923 | "scenario phrase wallet can be created") ? 0 : 1; | ||
| 924 | |||
| 925 | if (i == 0) | ||
| 926 | { | ||
| 927 | string strBlockedOut = tmp + "/" + scenarios[i].pszName + "-blocked-mutate.txt"; | ||
| 928 | vector<string> vBlockedArgs; | ||
| 929 | vBlockedArgs.push_back("-datadir=" + strScenarioDir); | ||
| 930 | vBlockedArgs.push_back("-nomanagedtor"); | ||
| 931 | vBlockedArgs.push_back("-nogui"); | ||
| 932 | vBlockedArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); | ||
| 933 | int nBlockedRet = RunBitflashChild(strExe, vBlockedArgs, NULL, &strBlockedOut); | ||
| 934 | nFail += Check(nBlockedRet != 0 && | ||
| 935 | FileContainsText(strBlockedOut, "Refusing destructive wallet storage mutation"), | ||
| 936 | "wallet mutation helper is blocked without marker") ? 0 : 1; | ||
| 937 | } | ||
| 938 | |||
| 939 | nFail += Check(WriteSelfTestMutationMarker(strScenarioDir), | ||
| 940 | "scenario mutation marker can be written") ? 0 : 1; | ||
| 941 | |||
| 942 | string strMutateOut = tmp + "/" + scenarios[i].pszName + "-mutate.txt"; | ||
| 943 | vector<string> vMutateArgs; | ||
| 944 | vMutateArgs.push_back("-datadir=" + strScenarioDir); | ||
| 945 | vMutateArgs.push_back("-nomanagedtor"); | ||
| 946 | vMutateArgs.push_back("-nogui"); | ||
| 947 | vMutateArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); | ||
| 948 | int nMutateRet = RunBitflashChild(strExe, vMutateArgs, NULL, &strMutateOut); | ||
| 949 | nFail += Check(nMutateRet == 0, scenarios[i].pszName) ? 0 : 1; | ||
| 950 | |||
| 951 | string strCheckOut = tmp + "/" + scenarios[i].pszName + "-check.txt"; | ||
| 952 | vector<string> vCheckArgs; | ||
| 953 | vCheckArgs.push_back("-datadir=" + strScenarioDir); | ||
| 954 | vCheckArgs.push_back("-nomanagedtor"); | ||
| 955 | vCheckArgs.push_back("-nogui"); | ||
| 956 | vCheckArgs.push_back("-walletstoragecheck"); | ||
| 957 | int nCheckRet = RunBitflashChild(strExe, vCheckArgs, NULL, &strCheckOut); | ||
| 958 | nFail += Check(nCheckRet == 2 && | ||
| 959 | FileContainsText(strCheckOut, "storage sanity: failed") && | ||
| 960 | FileContainsText(strCheckOut, scenarios[i].pszExpectedFailure), | ||
| 961 | scenarios[i].pszExpectedFailure) ? 0 : 1; | ||
| 962 | } | ||
| 963 | } | ||
| 964 | catch (const std::exception& e) | ||
| 965 | { | ||
| 966 | printf(" FAIL exception: %s\n", e.what()); | ||
| 967 | nFail++; | ||
| 968 | } | ||
| 969 | catch (...) | ||
| 970 | { | ||
| 971 | printf(" FAIL unknown exception\n"); | ||
| 972 | nFail++; | ||
| 973 | } | ||
| 974 | |||
| 975 | RemoveTree(tmp); | ||
| 976 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 977 | nFail, nFail == 1 ? "" : "s"); | ||
| 978 | fflush(stdout); | ||
| 979 | return nFail == 0 ? 0 : 1; | ||
| 980 | } | ||
| 981 | |||
| 982 | static int RunDbEnvReopenSelfTest() | ||
| 983 | { | ||
| 984 | fflush(stdout); | ||
| 985 | printf("db-env-reopen self-test\n"); | ||
| 986 | |||
| 987 | std::string tmp; | ||
| 988 | if (!MakeTempDir(tmp)) | ||
| 989 | { | ||
| 990 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 991 | return 1; | ||
| 992 | } | ||
| 993 | |||
| 994 | int nFail = 0; | ||
| 995 | std::string strOldDataDir = strSetDataDir; | ||
| 996 | strSetDataDir = tmp; | ||
| 997 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 998 | |||
| 999 | try | ||
| 1000 | { | ||
| 1001 | for (int i = 0; i < 5; i++) | ||
| 1002 | { | ||
| 1003 | int64 nValue = 1000 + i; | ||
| 1004 | { | ||
| 1005 | CWalletDB walletdb("cr+"); | ||
| 1006 | nFail += Check(walletdb.WriteSetting("db-env-reopen-cycle", nValue), | ||
| 1007 | "wallet setting can be written before shutdown") ? 0 : 1; | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | DBFlush(true); | ||
| 1011 | |||
| 1012 | int64 nRead = 0; | ||
| 1013 | { | ||
| 1014 | CWalletDB walletdb("r"); | ||
| 1015 | nFail += Check(walletdb.ReadSetting("db-env-reopen-cycle", nRead), | ||
| 1016 | "wallet setting can be read after env reopen") ? 0 : 1; | ||
| 1017 | } | ||
| 1018 | nFail += Check(nRead == nValue, | ||
| 1019 | "reopened environment reads the last committed value") ? 0 : 1; | ||
| 1020 | |||
| 1021 | DBFlush(true); | ||
| 1022 | } | ||
| 1023 | } | ||
| 1024 | catch (const std::exception& e) | ||
| 1025 | { | ||
| 1026 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1027 | nFail++; | ||
| 1028 | } | ||
| 1029 | catch (...) | ||
| 1030 | { | ||
| 1031 | printf(" FAIL unknown exception\n"); | ||
| 1032 | nFail++; | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | DBFlush(true); | ||
| 1036 | strSetDataDir = strOldDataDir; | ||
| 1037 | RemoveTree(tmp); | ||
| 1038 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1039 | nFail, nFail == 1 ? "" : "s"); | ||
| 1040 | fflush(stdout); | ||
| 1041 | return nFail == 0 ? 0 : 1; | ||
| 1042 | } | ||
| 1043 | |||
| 1044 | static int RunWalletSQLiteSelfTest() | ||
| 1045 | { | ||
| 1046 | fflush(stdout); | ||
| 1047 | printf("wallet-sqlite self-test\n"); | ||
| 1048 | |||
| 1049 | std::string tmp; | ||
| 1050 | if (!MakeTempDir(tmp)) | ||
| 1051 | { | ||
| 1052 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 1053 | return 1; | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | int nFail = 0; | ||
| 1057 | string strPath = tmp + "/wallet.sqlite"; | ||
| 1058 | string strError; | ||
| 1059 | string strOldRuntimeDataDir; | ||
| 1060 | bool fRestoreRuntimeDataDir = false; | ||
| 1061 | |||
| 1062 | try | ||
| 1063 | { | ||
| 1064 | CDataStream ssKeySetting(SER_DISK); | ||
| 1065 | ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip")); | ||
| 1066 | CDataStream ssValueSetting(SER_DISK); | ||
| 1067 | ssValueSetting << (int64)424242; | ||
| 1068 | |||
| 1069 | vector<unsigned char> vchPubKey; | ||
| 1070 | vchPubKey.push_back(0x04); | ||
| 1071 | for (int i = 0; i < 64; i++) | ||
| 1072 | vchPubKey.push_back((unsigned char)i); | ||
| 1073 | CDataStream ssKeyDefault(SER_DISK); | ||
| 1074 | ssKeyDefault << string("defaultkey"); | ||
| 1075 | CDataStream ssValueDefault(SER_DISK); | ||
| 1076 | ssValueDefault << vchPubKey; | ||
| 1077 | |||
| 1078 | { | ||
| 1079 | CWalletDBSQLite db; | ||
| 1080 | nFail += Check(db.Open(strPath, strError), | ||
| 1081 | "SQLite wallet opens and creates schema") ? 0 : 1; | ||
| 1082 | nFail += Check(db.BeginTransaction(strError), | ||
| 1083 | "SQLite wallet begins a write transaction") ? 0 : 1; | ||
| 1084 | nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting), | ||
| 1085 | DataStreamBytes(ssValueSetting), | ||
| 1086 | strError), | ||
| 1087 | "SQLite wallet writes a serialized setting record") ? 0 : 1; | ||
| 1088 | nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault), | ||
| 1089 | DataStreamBytes(ssValueDefault), | ||
| 1090 | strError), | ||
| 1091 | "SQLite wallet writes a serialized defaultkey record") ? 0 : 1; | ||
| 1092 | nFail += Check(db.CommitTransaction(strError), | ||
| 1093 | "SQLite wallet commits a write transaction") ? 0 : 1; | ||
| 1094 | int nCount = 0; | ||
| 1095 | nFail += Check(db.CountRecords(nCount, strError) && nCount == 2, | ||
| 1096 | "SQLite wallet counts raw records") ? 0 : 1; | ||
| 1097 | } | ||
| 1098 | |||
| 1099 | { | ||
| 1100 | CWalletDBSQLite db; | ||
| 1101 | nFail += Check(db.Open(strPath, strError), | ||
| 1102 | "SQLite wallet reopens") ? 0 : 1; | ||
| 1103 | vector<unsigned char> vchRead; | ||
| 1104 | nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting), | ||
| 1105 | vchRead, | ||
| 1106 | strError), | ||
| 1107 | "SQLite wallet reads a serialized setting record") ? 0 : 1; | ||
| 1108 | int64 nReadValue = 0; | ||
| 1109 | CDataStream ssRead(vchRead, SER_DISK); | ||
| 1110 | ssRead >> nReadValue; | ||
| 1111 | nFail += Check(!ssRead.fail() && nReadValue == 424242, | ||
| 1112 | "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1; | ||
| 1113 | |||
| 1114 | vchRead.clear(); | ||
| 1115 | nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault), | ||
| 1116 | vchRead, | ||
| 1117 | strError), | ||
| 1118 | "SQLite wallet reads a serialized defaultkey record") ? 0 : 1; | ||
| 1119 | vector<unsigned char> vchReadPubKey; | ||
| 1120 | CDataStream ssReadDefault(vchRead, SER_DISK); | ||
| 1121 | ssReadDefault >> vchReadPubKey; | ||
| 1122 | nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey, | ||
| 1123 | "SQLite wallet preserves public key blobs") ? 0 : 1; | ||
| 1124 | } | ||
| 1125 | |||
| 1126 | { | ||
| 1127 | CWalletDBSQLite db; | ||
| 1128 | nFail += Check(db.Open(strPath, strError), | ||
| 1129 | "SQLite wallet reopens for typed records") ? 0 : 1; | ||
| 1130 | |||
| 1131 | nFail += Check(db.WriteTypedRecord(make_pair(string("setting"), | ||
| 1132 | string("sqlite-typed")), | ||
| 1133 | (int64)777, strError), | ||
| 1134 | "SQLite wallet writes a typed setting record") ? 0 : 1; | ||
| 1135 | |||
| 1136 | int64 nTypedRead = 0; | ||
| 1137 | nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1138 | string("sqlite-typed")), | ||
| 1139 | nTypedRead, strError) && | ||
| 1140 | nTypedRead == 777, | ||
| 1141 | "SQLite wallet reads a typed setting record") ? 0 : 1; | ||
| 1142 | |||
| 1143 | nFail += Check(!db.WriteTypedRecord(make_pair(string("setting"), | ||
| 1144 | string("sqlite-typed")), | ||
| 1145 | (int64)888, strError, false), | ||
| 1146 | "SQLite wallet refuses typed overwrite when asked") ? 0 : 1; | ||
| 1147 | |||
| 1148 | nTypedRead = 0; | ||
| 1149 | nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1150 | string("sqlite-typed")), | ||
| 1151 | nTypedRead, strError) && | ||
| 1152 | nTypedRead == 777, | ||
| 1153 | "SQLite wallet preserves the old typed value after overwrite refusal") ? 0 : 1; | ||
| 1154 | |||
| 1155 | nFail += Check(db.EraseTypedRecord(make_pair(string("setting"), | ||
| 1156 | string("sqlite-typed")), | ||
| 1157 | strError), | ||
| 1158 | "SQLite wallet erases a typed record") ? 0 : 1; | ||
| 1159 | |||
| 1160 | nTypedRead = 0; | ||
| 1161 | nFail += Check(!db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1162 | string("sqlite-typed")), | ||
| 1163 | nTypedRead, strError), | ||
| 1164 | "SQLite wallet typed erase removes the record") ? 0 : 1; | ||
| 1165 | |||
| 1166 | nFail += Check(db.WriteTypedRecord(string("defaultkey"), vchPubKey, | ||
| 1167 | strError), | ||
| 1168 | "SQLite wallet writes a typed defaultkey record") ? 0 : 1; | ||
| 1169 | |||
| 1170 | vector<unsigned char> vchTypedPubKey; | ||
| 1171 | nFail += Check(db.ReadTypedRecord(string("defaultkey"), vchTypedPubKey, | ||
| 1172 | strError) && | ||
| 1173 | vchTypedPubKey == vchPubKey, | ||
| 1174 | "SQLite wallet reads a typed defaultkey record") ? 0 : 1; | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | { | ||
| 1178 | string strRuntimePath = tmp + "/runtime-wallet.sqlite"; | ||
| 1179 | string strRuntimeDir = tmp + "/runtime-datadir"; | ||
| 1180 | nFail += Check(MakeDirLocal(strRuntimeDir), | ||
| 1181 | "SQLite runtime datadir can be created") ? 0 : 1; | ||
| 1182 | |||
| 1183 | { | ||
| 1184 | CWalletDBSQLite db; | ||
| 1185 | nFail += Check(db.Open(strRuntimePath, strError), | ||
| 1186 | "SQLite runtime wallet file can be created") ? 0 : 1; | ||
| 1187 | } | ||
| 1188 | |||
| 1189 | string strOldDataDir = strSetDataDir; | ||
| 1190 | strSetDataDir = strRuntimeDir; | ||
| 1191 | strOldRuntimeDataDir = strOldDataDir; | ||
| 1192 | fRestoreRuntimeDataDir = true; | ||
| 1193 | bool fRuntimeOpened = WalletSQLiteRuntimeOpen(strRuntimePath, strError); | ||
| 1194 | nFail += Check(fRuntimeOpened, | ||
| 1195 | "SQLite runtime wallet can be activated") ? 0 : 1; | ||
| 1196 | |||
| 1197 | if (fRuntimeOpened) | ||
| 1198 | { | ||
| 1199 | CKey key; | ||
| 1200 | key.MakeNewKey(); | ||
| 1201 | vector<unsigned char> vchRuntimePubKey = key.GetPubKey(); | ||
| 1202 | CPrivKey vchRuntimePrivKey = key.GetPrivKey(); | ||
| 1203 | string strRuntimeAddress = PubKeyToAddress(vchRuntimePubKey); | ||
| 1204 | |||
| 1205 | nFail += Check(CWalletDB().WriteSetting("runtime-sqlite-setting", | ||
| 1206 | (int64)909090) && | ||
| 1207 | CWalletDB().WriteDefaultKey(vchRuntimePubKey) && | ||
| 1208 | CWalletDB().WriteKey(vchRuntimePubKey, | ||
| 1209 | vchRuntimePrivKey) && | ||
| 1210 | CWalletDB().WriteName(strRuntimeAddress, | ||
| 1211 | "SQLite Runtime Address"), | ||
| 1212 | "CWalletDB writes route to active SQLite runtime") ? 0 : 1; | ||
| 1213 | |||
| 1214 | int64 nRuntimeSetting = 0; | ||
| 1215 | vector<unsigned char> vchRuntimeDefaultKey; | ||
| 1216 | CPrivKey vchRuntimeReadPrivKey; | ||
| 1217 | string strRuntimeName; | ||
| 1218 | nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", | ||
| 1219 | nRuntimeSetting) && | ||
| 1220 | nRuntimeSetting == 909090 && | ||
| 1221 | CWalletDB("r").ReadDefaultKey(vchRuntimeDefaultKey) && | ||
| 1222 | vchRuntimeDefaultKey == vchRuntimePubKey && | ||
| 1223 | CWalletDB("r").ReadKey(vchRuntimePubKey, | ||
| 1224 | vchRuntimeReadPrivKey) && | ||
| 1225 | vchRuntimeReadPrivKey == vchRuntimePrivKey && | ||
| 1226 | CWalletDB("r").ReadName(strRuntimeAddress, | ||
| 1227 | strRuntimeName) && | ||
| 1228 | strRuntimeName == "SQLite Runtime Address", | ||
| 1229 | "CWalletDB reads route to active SQLite runtime") ? 0 : 1; | ||
| 1230 | |||
| 1231 | nFail += Check(CWalletDB().EraseName(strRuntimeAddress) && | ||
| 1232 | !CWalletDB("r").ReadName(strRuntimeAddress, | ||
| 1233 | strRuntimeName), | ||
| 1234 | "CWalletDB erases route to active SQLite runtime") ? 0 : 1; | ||
| 1235 | |||
| 1236 | nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), | ||
| 1237 | "active SQLite runtime does not create wallet.dat") ? 0 : 1; | ||
| 1238 | |||
| 1239 | // Transaction bracket: a rolled-back group leaves nothing, a | ||
| 1240 | // committed group persists. This is the all-or-nothing that | ||
| 1241 | // WriteHDMaster relies on so a half-written seed cannot survive | ||
| 1242 | // a failure between its two records. | ||
| 1243 | bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); | ||
| 1244 | bool fTxnWrote = fTxnBegan && | ||
| 1245 | CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); | ||
| 1246 | if (fTxnBegan) | ||
| 1247 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 1248 | nFail += Check(fTxnBegan && fTxnWrote, | ||
| 1249 | "a SQLite runtime transaction opens and rolls back") ? 0 : 1; | ||
| 1250 | int64 nRolledBack = 0; | ||
| 1251 | nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", | ||
| 1252 | nRolledBack), | ||
| 1253 | "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; | ||
| 1254 | |||
| 1255 | bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && | ||
| 1256 | CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && | ||
| 1257 | WalletSQLiteRuntimeCommitTxn(); | ||
| 1258 | int64 nCommitted = 0; | ||
| 1259 | nFail += Check(fCommitOk && | ||
| 1260 | CWalletDB("r").ReadSetting("runtime-txn-commit", | ||
| 1261 | nCommitted) && | ||
| 1262 | nCommitted == 222, | ||
| 1263 | "a committed SQLite runtime write persists") ? 0 : 1; | ||
| 1264 | |||
| 1265 | // WriteHDMaster brackets its two records in that transaction; | ||
| 1266 | // both halves of the seed must land. | ||
| 1267 | vector<unsigned char> vchTxnMaster(32, 0x5a); | ||
| 1268 | vector<unsigned char> vchTxnChainCode(32, 0xa5); | ||
| 1269 | vector<unsigned char> vchReadMaster, vchReadChainCode; | ||
| 1270 | nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && | ||
| 1271 | CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && | ||
| 1272 | vchReadMaster == vchTxnMaster && | ||
| 1273 | vchReadChainCode == vchTxnChainCode, | ||
| 1274 | "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1; | ||
| 1275 | |||
| 1276 | // Durability: close the backend (checkpoints the WAL), reopen | ||
| 1277 | // the same file, and confirm what CWalletDB wrote is still | ||
| 1278 | // there. This is the write-survives-a-restart property the real | ||
| 1279 | // -walletbackend=sqlite node depends on. | ||
| 1280 | WalletSQLiteRuntimeClose(); | ||
| 1281 | bool fReopened = | ||
| 1282 | WalletSQLiteRuntimeOpen(strRuntimePath, strError); | ||
| 1283 | nFail += Check(fReopened, | ||
| 1284 | "SQLite runtime wallet reopens after close") ? 0 : 1; | ||
| 1285 | if (fReopened) | ||
| 1286 | { | ||
| 1287 | int64 nPersisted = 0; | ||
| 1288 | CPrivKey vchPersistedPrivKey; | ||
| 1289 | vector<unsigned char> vchPersistedDefaultKey; | ||
| 1290 | nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", | ||
| 1291 | nPersisted) && | ||
| 1292 | nPersisted == 909090 && | ||
| 1293 | CWalletDB("r").ReadDefaultKey(vchPersistedDefaultKey) && | ||
| 1294 | vchPersistedDefaultKey == vchRuntimePubKey && | ||
| 1295 | CWalletDB("r").ReadKey(vchRuntimePubKey, | ||
| 1296 | vchPersistedPrivKey) && | ||
| 1297 | vchPersistedPrivKey == vchRuntimePrivKey, | ||
| 1298 | "SQLite runtime writes persist across a close and reopen") ? 0 : 1; | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | WalletSQLiteRuntimeClose(); | ||
| 1303 | strSetDataDir = strOldDataDir; | ||
| 1304 | fRestoreRuntimeDataDir = false; | ||
| 1305 | } | ||
| 1306 | } | ||
| 1307 | catch (const std::exception& e) | ||
| 1308 | { | ||
| 1309 | WalletSQLiteRuntimeClose(); | ||
| 1310 | if (fRestoreRuntimeDataDir) | ||
| 1311 | strSetDataDir = strOldRuntimeDataDir; | ||
| 1312 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1313 | nFail++; | ||
| 1314 | } | ||
| 1315 | catch (...) | ||
| 1316 | { | ||
| 1317 | WalletSQLiteRuntimeClose(); | ||
| 1318 | if (fRestoreRuntimeDataDir) | ||
| 1319 | strSetDataDir = strOldRuntimeDataDir; | ||
| 1320 | printf(" FAIL unknown exception\n"); | ||
| 1321 | nFail++; | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | RemoveTree(tmp); | ||
| 1325 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1326 | nFail, nFail == 1 ? "" : "s"); | ||
| 1327 | fflush(stdout); | ||
| 1328 | return nFail == 0 ? 0 : 1; | ||
| 1329 | } | ||
| 1330 | |||
| 1331 | namespace { | ||
| 1332 | |||
| 1333 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 1334 | { | ||
| 1335 | public: | ||
| 1336 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords; | ||
| 1337 | |||
| 1338 | explicit CWalletRecordMapVisitor( | ||
| 1339 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn) | ||
| 1340 | : mapRecords(mapRecordsIn) { } | ||
| 1341 | |||
| 1342 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 1343 | const CDataStream& ssValue, | ||
| 1344 | string& strErrorRet) | ||
| 1345 | { | ||
| 1346 | vector<unsigned char> vchKey = DataStreamBytes(ssKey); | ||
| 1347 | if (mapRecords.count(vchKey)) | ||
| 1348 | { | ||
| 1349 | strErrorRet = "duplicate serialized wallet key"; | ||
| 1350 | return false; | ||
| 1351 | } | ||
| 1352 | mapRecords[vchKey] = DataStreamBytes(ssValue); | ||
| 1353 | return true; | ||
| 1354 | } | ||
| 1355 | }; | ||
| 1356 | |||
| 1357 | } // namespace | ||
| 1358 | |||
| 1359 | static bool CreateSQLiteWalletWithEmptyKey(const string& strPath) | ||
| 1360 | { | ||
| 1361 | sqlite3* pdb = NULL; | ||
| 1362 | int ret = sqlite3_open_v2(strPath.c_str(), &pdb, | ||
| 1363 | SQLITE_OPEN_READWRITE | | ||
| 1364 | SQLITE_OPEN_CREATE | | ||
| 1365 | SQLITE_OPEN_FULLMUTEX, | ||
| 1366 | NULL); | ||
| 1367 | if (ret != SQLITE_OK || !pdb) | ||
| 1368 | { | ||
| 1369 | if (pdb) | ||
| 1370 | sqlite3_close(pdb); | ||
| 1371 | return false; | ||
| 1372 | } | ||
| 1373 | |||
| 1374 | char* pszErr = NULL; | ||
| 1375 | bool fOk = | ||
| 1376 | sqlite3_exec(pdb, | ||
| 1377 | "CREATE TABLE wallet_records (" | ||
| 1378 | "key BLOB PRIMARY KEY NOT NULL," | ||
| 1379 | "value BLOB NOT NULL" | ||
| 1380 | ");", | ||
| 1381 | NULL, NULL, &pszErr) == SQLITE_OK && | ||
| 1382 | sqlite3_exec(pdb, | ||
| 1383 | "INSERT INTO wallet_records(key, value) " | ||
| 1384 | "VALUES(x'', x'01');", | ||
| 1385 | NULL, NULL, &pszErr) == SQLITE_OK; | ||
| 1386 | if (pszErr) | ||
| 1387 | sqlite3_free(pszErr); | ||
| 1388 | sqlite3_close(pdb); | ||
| 1389 | return fOk; | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath) | ||
| 1393 | { | ||
| 1394 | string strError; | ||
| 1395 | CWalletDBSQLite db; | ||
| 1396 | CDataStream ssKey(SER_DISK); | ||
| 1397 | ssKey << string("walletminversion"); | ||
| 1398 | CDataStream ssValue(SER_DISK); | ||
| 1399 | ssValue << (WALLET_FORMAT_SUPPORTED + 1); | ||
| 1400 | |||
| 1401 | if (!db.Open(strPath, strError)) | ||
| 1402 | return false; | ||
| 1403 | if (!db.BeginTransaction(strError)) | ||
| 1404 | return false; | ||
| 1405 | if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError)) | ||
| 1406 | return false; | ||
| 1407 | if (!db.CommitTransaction(strError)) | ||
| 1408 | return false; | ||
| 1409 | if (!db.Checkpoint(strError)) | ||
| 1410 | return false; | ||
| 1411 | return true; | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | static bool CreateTypedSQLiteWallet(const string& strPath) | ||
| 1415 | { | ||
| 1416 | string strError; | ||
| 1417 | CWalletDBSQLite db; | ||
| 1418 | CKey key; | ||
| 1419 | key.MakeNewKey(); | ||
| 1420 | |||
| 1421 | vector<unsigned char> vchPubKey = key.GetPubKey(); | ||
| 1422 | CPrivKey vchPrivKey = key.GetPrivKey(); | ||
| 1423 | string strAddress = PubKeyToAddress(vchPubKey); | ||
| 1424 | |||
| 1425 | if (!db.Open(strPath, strError)) | ||
| 1426 | return false; | ||
| 1427 | if (!db.BeginTransaction(strError)) | ||
| 1428 | return false; | ||
| 1429 | if (!db.WriteTypedRecord(make_pair(string("key"), vchPubKey), | ||
| 1430 | vchPrivKey, strError, false)) | ||
| 1431 | return false; | ||
| 1432 | if (!db.WriteTypedRecord(string("defaultkey"), vchPubKey, strError)) | ||
| 1433 | return false; | ||
| 1434 | if (!db.WriteTypedRecord(make_pair(string("name"), strAddress), | ||
| 1435 | string("Typed SQLite Address"), strError)) | ||
| 1436 | return false; | ||
| 1437 | if (!db.CommitTransaction(strError)) | ||
| 1438 | return false; | ||
| 1439 | if (!db.Checkpoint(strError)) | ||
| 1440 | return false; | ||
| 1441 | return true; | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | static int RunWalletSQLiteMigrationSelfTest() | ||
| 1445 | { | ||
| 1446 | fflush(stdout); | ||
| 1447 | printf("wallet-sqlite-migration self-test\n"); | ||
| 1448 | |||
| 1449 | std::string tmp; | ||
| 1450 | std::string cwd; | ||
| 1451 | if (!MakeTempDir(tmp)) | ||
| 1452 | { | ||
| 1453 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 1454 | return 1; | ||
| 1455 | } | ||
| 1456 | if (!GetCurrentDir(cwd)) | ||
| 1457 | { | ||
| 1458 | printf(" FAIL could not read current directory\n"); | ||
| 1459 | RemoveTree(tmp); | ||
| 1460 | return 1; | ||
| 1461 | } | ||
| 1462 | |||
| 1463 | int nFail = 0; | ||
| 1464 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 1465 | |||
| 1466 | try | ||
| 1467 | { | ||
| 1468 | #ifdef _WIN32 | ||
| 1469 | string strExe = cwd + "\\bitflash.exe"; | ||
| 1470 | #else | ||
| 1471 | string strExe = cwd + "/bitflash-node"; | ||
| 1472 | #endif | ||
| 1473 | string strWalletDir = tmp + "/wallet"; | ||
| 1474 | nFail += Check(MakeDirLocal(strWalletDir), | ||
| 1475 | "temporary wallet directory can be created") ? 0 : 1; | ||
| 1476 | |||
| 1477 | string strCreateOut = tmp + "/create.txt"; | ||
| 1478 | vector<string> vCreateArgs; | ||
| 1479 | vCreateArgs.push_back("-datadir=" + strWalletDir); | ||
| 1480 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 1481 | vCreateArgs.push_back("-nogui"); | ||
| 1482 | vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat | ||
| 1483 | vCreateArgs.push_back("-newphrase"); | ||
| 1484 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 1485 | nFail += Check(nCreateRet == 0 && | ||
| 1486 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 1487 | "a phrase wallet can be created for SQLite export") ? 0 : 1; | ||
| 1488 | |||
| 1489 | string strSQLite = tmp + "/wallet.sqlite"; | ||
| 1490 | string strExportOut = tmp + "/sqlite-export.txt"; | ||
| 1491 | vector<string> vExportArgs; | ||
| 1492 | vExportArgs.push_back("-datadir=" + strWalletDir); | ||
| 1493 | vExportArgs.push_back("-nomanagedtor"); | ||
| 1494 | vExportArgs.push_back("-nogui"); | ||
| 1495 | vExportArgs.push_back("-walletsqliteexport=" + strSQLite); | ||
| 1496 | int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); | ||
| 1497 | nFail += Check(nExportRet == 0 && | ||
| 1498 | FileExists(strSQLite.c_str()) && | ||
| 1499 | FileContainsText(strExportOut, "SQLite wallet export written"), | ||
| 1500 | "wallet.dat can be exported to SQLite") ? 0 : 1; | ||
| 1501 | |||
| 1502 | string strVerifyOut = tmp + "/sqlite-verify.txt"; | ||
| 1503 | vector<string> vVerifyArgs; | ||
| 1504 | vVerifyArgs.push_back("-datadir=" + strWalletDir); | ||
| 1505 | vVerifyArgs.push_back("-nomanagedtor"); | ||
| 1506 | vVerifyArgs.push_back("-nogui"); | ||
| 1507 | vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite); | ||
| 1508 | int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut); | ||
| 1509 | nFail += Check(nVerifyRet == 0 && | ||
| 1510 | FileContainsText(strVerifyOut, "verification: ok"), | ||
| 1511 | "SQLite wallet export verifies against wallet.dat") ? 0 : 1; | ||
| 1512 | |||
| 1513 | string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt"; | ||
| 1514 | vector<string> vLoadCheckArgs; | ||
| 1515 | vLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1516 | vLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1517 | vLoadCheckArgs.push_back("-nogui"); | ||
| 1518 | vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite); | ||
| 1519 | int nLoadCheckRet = | ||
| 1520 | RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut); | ||
| 1521 | nFail += Check(nLoadCheckRet == 0 && | ||
| 1522 | FileContainsText(strLoadCheckOut, | ||
| 1523 | "load check: ok"), | ||
| 1524 | "SQLite wallet export passes the loader compatibility check") ? 0 : 1; | ||
| 1525 | |||
| 1526 | string strRuntimeOut = tmp + "/sqlite-runtime.txt"; | ||
| 1527 | vector<string> vRuntimeArgs; | ||
| 1528 | vRuntimeArgs.push_back("-datadir=" + strWalletDir); | ||
| 1529 | vRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1530 | vRuntimeArgs.push_back("-nogui"); | ||
| 1531 | vRuntimeArgs.push_back("-walletsqlite=" + strSQLite); | ||
| 1532 | int nRuntimeRet = | ||
| 1533 | RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut); | ||
| 1534 | nFail += Check(nRuntimeRet == 0 && | ||
| 1535 | FileContainsText(strRuntimeOut, | ||
| 1536 | "SQLite wallet loaded: read-only staging backend"), | ||
| 1537 | "SQLite wallet export loads through the runtime startup path") ? 0 : 1; | ||
| 1538 | |||
| 1539 | string strTypedSQLite = tmp + "/typed-wallet.sqlite"; | ||
| 1540 | nFail += Check(CreateTypedSQLiteWallet(strTypedSQLite), | ||
| 1541 | "typed SQLite wallet can be created without Berkeley DB") ? 0 : 1; | ||
| 1542 | |||
| 1543 | string strTypedLoadCheckOut = tmp + "/sqlite-typed-loadcheck.txt"; | ||
| 1544 | vector<string> vTypedLoadCheckArgs; | ||
| 1545 | vTypedLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1546 | vTypedLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1547 | vTypedLoadCheckArgs.push_back("-nogui"); | ||
| 1548 | vTypedLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strTypedSQLite); | ||
| 1549 | int nTypedLoadCheckRet = | ||
| 1550 | RunBitflashChild(strExe, vTypedLoadCheckArgs, NULL, | ||
| 1551 | &strTypedLoadCheckOut); | ||
| 1552 | nFail += Check(nTypedLoadCheckRet == 0 && | ||
| 1553 | FileContainsText(strTypedLoadCheckOut, | ||
| 1554 | "load check: ok"), | ||
| 1555 | "typed SQLite wallet passes the loader compatibility check") ? 0 : 1; | ||
| 1556 | |||
| 1557 | string strTypedRuntimeDir = tmp + "/typed-runtime-wallet"; | ||
| 1558 | nFail += Check(MakeDirLocal(strTypedRuntimeDir), | ||
| 1559 | "typed SQLite runtime directory can be created") ? 0 : 1; | ||
| 1560 | string strTypedRuntimeOut = tmp + "/sqlite-typed-runtime.txt"; | ||
| 1561 | vector<string> vTypedRuntimeArgs; | ||
| 1562 | vTypedRuntimeArgs.push_back("-datadir=" + strTypedRuntimeDir); | ||
| 1563 | vTypedRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1564 | vTypedRuntimeArgs.push_back("-nogui"); | ||
| 1565 | vTypedRuntimeArgs.push_back("-walletsqlite=" + strTypedSQLite); | ||
| 1566 | int nTypedRuntimeRet = | ||
| 1567 | RunBitflashChild(strExe, vTypedRuntimeArgs, NULL, | ||
| 1568 | &strTypedRuntimeOut); | ||
| 1569 | nFail += Check(nTypedRuntimeRet == 0 && | ||
| 1570 | FileContainsText(strTypedRuntimeOut, | ||
| 1571 | "SQLite wallet loaded: read-only staging backend") && | ||
| 1572 | !FileExists((strTypedRuntimeDir + "/wallet.dat").c_str()), | ||
| 1573 | "typed SQLite wallet loads through runtime without creating wallet.dat") ? 0 : 1; | ||
| 1574 | |||
| 1575 | string strRestoredDir = tmp + "/restored-wallet"; | ||
| 1576 | nFail += Check(MakeDirLocal(strRestoredDir), | ||
| 1577 | "restored wallet directory can be created") ? 0 : 1; | ||
| 1578 | string strRestoreOut = tmp + "/sqlite-restore.txt"; | ||
| 1579 | vector<string> vRestoreArgs; | ||
| 1580 | vRestoreArgs.push_back("-datadir=" + strRestoredDir); | ||
| 1581 | vRestoreArgs.push_back("-nomanagedtor"); | ||
| 1582 | vRestoreArgs.push_back("-nogui"); | ||
| 1583 | vRestoreArgs.push_back("-walletsqliterestore=" + strSQLite); | ||
| 1584 | int nRestoreRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOut); | ||
| 1585 | nFail += Check(nRestoreRet == 0 && | ||
| 1586 | FileContainsText(strRestoreOut, "records restored:"), | ||
| 1587 | "SQLite wallet export restores into a fresh wallet.dat") ? 0 : 1; | ||
| 1588 | |||
| 1589 | string strVerifyRestoredOut = tmp + "/sqlite-verify-restored.txt"; | ||
| 1590 | vector<string> vVerifyRestoredArgs; | ||
| 1591 | vVerifyRestoredArgs.push_back("-datadir=" + strRestoredDir); | ||
| 1592 | vVerifyRestoredArgs.push_back("-nomanagedtor"); | ||
| 1593 | vVerifyRestoredArgs.push_back("-nogui"); | ||
| 1594 | vVerifyRestoredArgs.push_back("-walletsqliteverify=" + strSQLite); | ||
| 1595 | int nVerifyRestoredRet = | ||
| 1596 | RunBitflashChild(strExe, vVerifyRestoredArgs, NULL, &strVerifyRestoredOut); | ||
| 1597 | nFail += Check(nVerifyRestoredRet == 0 && | ||
| 1598 | FileContainsText(strVerifyRestoredOut, | ||
| 1599 | "verification: ok"), | ||
| 1600 | "restored wallet.dat verifies against the SQLite export") ? 0 : 1; | ||
| 1601 | |||
| 1602 | string strRestoreOverwriteOut = tmp + "/sqlite-restore-overwrite.txt"; | ||
| 1603 | int nRestoreOverwriteRet = | ||
| 1604 | RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOverwriteOut); | ||
| 1605 | nFail += Check(nRestoreOverwriteRet == 1 && | ||
| 1606 | FileContainsText(strRestoreOverwriteOut, | ||
| 1607 | "Refusing to overwrite existing wallet.dat"), | ||
| 1608 | "SQLite wallet restore refuses to overwrite wallet.dat") ? 0 : 1; | ||
| 1609 | |||
| 1610 | string strBadSQLite = tmp + "/bad-empty-key.sqlite"; | ||
| 1611 | nFail += Check(CreateSQLiteWalletWithEmptyKey(strBadSQLite), | ||
| 1612 | "bad SQLite export with an empty key can be created") ? 0 : 1; | ||
| 1613 | string strBadRestoreDir = tmp + "/bad-restore-wallet"; | ||
| 1614 | nFail += Check(MakeDirLocal(strBadRestoreDir), | ||
| 1615 | "bad restore wallet directory can be created") ? 0 : 1; | ||
| 1616 | string strBadRestoreOut = tmp + "/sqlite-restore-bad.txt"; | ||
| 1617 | vector<string> vBadRestoreArgs; | ||
| 1618 | vBadRestoreArgs.push_back("-datadir=" + strBadRestoreDir); | ||
| 1619 | vBadRestoreArgs.push_back("-nomanagedtor"); | ||
| 1620 | vBadRestoreArgs.push_back("-nogui"); | ||
| 1621 | vBadRestoreArgs.push_back("-walletsqliterestore=" + strBadSQLite); | ||
| 1622 | int nBadRestoreRet = | ||
| 1623 | RunBitflashChild(strExe, vBadRestoreArgs, NULL, &strBadRestoreOut); | ||
| 1624 | nFail += Check(nBadRestoreRet == 1 && | ||
| 1625 | FileContainsText(strBadRestoreOut, | ||
| 1626 | "Cannot restore SQLite wallet export") && | ||
| 1627 | !FileExists((strBadRestoreDir + "/wallet.dat").c_str()), | ||
| 1628 | "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1; | ||
| 1629 | |||
| 1630 | string strFutureSQLite = tmp + "/future-wallet.sqlite"; | ||
| 1631 | nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite), | ||
| 1632 | "SQLite export with a future wallet format can be created") ? 0 : 1; | ||
| 1633 | string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt"; | ||
| 1634 | vector<string> vFutureLoadCheckArgs; | ||
| 1635 | vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1636 | vFutureLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1637 | vFutureLoadCheckArgs.push_back("-nogui"); | ||
| 1638 | vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite); | ||
| 1639 | int nFutureLoadCheckRet = | ||
| 1640 | RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL, | ||
| 1641 | &strFutureLoadCheckOut); | ||
| 1642 | nFail += Check(nFutureLoadCheckRet == 2 && | ||
| 1643 | FileContainsText(strFutureLoadCheckOut, | ||
| 1644 | "unsupported future wallet format") && | ||
| 1645 | FileContainsText(strFutureLoadCheckOut, | ||
| 1646 | "load check: failed"), | ||
| 1647 | "SQLite wallet load check rejects future wallet formats") ? 0 : 1; | ||
| 1648 | |||
| 1649 | string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt"; | ||
| 1650 | vector<string> vFutureRuntimeArgs; | ||
| 1651 | vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir); | ||
| 1652 | vFutureRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1653 | vFutureRuntimeArgs.push_back("-nogui"); | ||
| 1654 | vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite); | ||
| 1655 | int nFutureRuntimeRet = | ||
| 1656 | RunBitflashChild(strExe, vFutureRuntimeArgs, NULL, | ||
| 1657 | &strFutureRuntimeOut); | ||
| 1658 | nFail += Check(nFutureRuntimeRet == 1 && | ||
| 1659 | FileContainsText(strFutureRuntimeOut, | ||
| 1660 | "Cannot open SQLite wallet export") && | ||
| 1661 | FileContainsText(strFutureRuntimeOut, | ||
| 1662 | "newer version of Bitflash"), | ||
| 1663 | "SQLite runtime startup rejects future wallet formats") ? 0 : 1; | ||
| 1664 | |||
| 1665 | string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; | ||
| 1666 | int nOverwriteRet = | ||
| 1667 | RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); | ||
| 1668 | nFail += Check(nOverwriteRet != 0 && | ||
| 1669 | FileContainsText(strOverwriteOut, | ||
| 1670 | "Refusing to overwrite existing SQLite wallet export"), | ||
| 1671 | "SQLite wallet export refuses to overwrite") ? 0 : 1; | ||
| 1672 | remove((strSQLite + "-wal").c_str()); | ||
| 1673 | remove((strSQLite + "-shm").c_str()); | ||
| 1674 | |||
| 1675 | std::map<vector<unsigned char>, vector<unsigned char> > mapBDB; | ||
| 1676 | string strOldDataDir = strSetDataDir; | ||
| 1677 | string strScanError; | ||
| 1678 | strSetDataDir = strWalletDir; | ||
| 1679 | CWalletRecordMapVisitor visitor(mapBDB); | ||
| 1680 | bool fScanOk = ScanWalletRecords(visitor, strScanError); | ||
| 1681 | DBFlush(true); | ||
| 1682 | strSetDataDir = strOldDataDir; | ||
| 1683 | nFail += Check(fScanOk && !mapBDB.empty(), | ||
| 1684 | "wallet.dat records can be scanned for comparison") ? 0 : 1; | ||
| 1685 | |||
| 1686 | CWalletDBSQLite db; | ||
| 1687 | string strError; | ||
| 1688 | nFail += Check(db.Open(strSQLite, strError), | ||
| 1689 | "exported SQLite wallet opens") ? 0 : 1; | ||
| 1690 | int nSQLiteRecords = 0; | ||
| 1691 | nFail += Check(db.CountRecords(nSQLiteRecords, strError) && | ||
| 1692 | nSQLiteRecords == (int)mapBDB.size(), | ||
| 1693 | "SQLite export has the same record count as wallet.dat") ? 0 : 1; | ||
| 1694 | |||
| 1695 | bool fAllMatch = true; | ||
| 1696 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 1697 | mapBDB.begin(); it != mapBDB.end(); ++it) | ||
| 1698 | { | ||
| 1699 | vector<unsigned char> vchSQLiteValue; | ||
| 1700 | if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || | ||
| 1701 | vchSQLiteValue != it->second) | ||
| 1702 | { | ||
| 1703 | fAllMatch = false; | ||
| 1704 | break; | ||
| 1705 | } | ||
| 1706 | } | ||
| 1707 | nFail += Check(fAllMatch, | ||
| 1708 | "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; | ||
| 1709 | |||
| 1710 | std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite; | ||
| 1711 | CWalletRecordMapVisitor sqliteVisitor(mapSQLite); | ||
| 1712 | nFail += Check(db.ScanRecords(sqliteVisitor, strError) && | ||
| 1713 | mapSQLite == mapBDB, | ||
| 1714 | "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; | ||
| 1715 | |||
| 1716 | db.Close(); | ||
| 1717 | CWalletDBSQLite dbCorrupt; | ||
| 1718 | vector<unsigned char> vchBadValue; | ||
| 1719 | vchBadValue.push_back(0xba); | ||
| 1720 | vchBadValue.push_back(0xad); | ||
| 1721 | nFail += Check(!mapBDB.empty() && | ||
| 1722 | dbCorrupt.Open(strSQLite, strError) && | ||
| 1723 | dbCorrupt.BeginTransaction(strError) && | ||
| 1724 | dbCorrupt.WriteRecord(mapBDB.begin()->first, | ||
| 1725 | vchBadValue, | ||
| 1726 | strError) && | ||
| 1727 | dbCorrupt.CommitTransaction(strError) && | ||
| 1728 | dbCorrupt.Checkpoint(strError), | ||
| 1729 | "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1; | ||
| 1730 | dbCorrupt.Close(); | ||
| 1731 | |||
| 1732 | string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt"; | ||
| 1733 | int nVerifyBadRet = | ||
| 1734 | RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut); | ||
| 1735 | nFail += Check(nVerifyBadRet == 2 && | ||
| 1736 | FileContainsText(strVerifyBadOut, | ||
| 1737 | "record value mismatches: 1") && | ||
| 1738 | FileContainsText(strVerifyBadOut, | ||
| 1739 | "verification: failed"), | ||
| 1740 | "SQLite wallet verifier detects a mismatched record value") ? 0 : 1; | ||
| 1741 | } | ||
| 1742 | catch (const std::exception& e) | ||
| 1743 | { | ||
| 1744 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1745 | nFail++; | ||
| 1746 | } | ||
| 1747 | catch (...) | ||
| 1748 | { | ||
| 1749 | printf(" FAIL unknown exception\n"); | ||
| 1750 | nFail++; | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | RemoveTree(tmp); | ||
| 1754 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1755 | nFail, nFail == 1 ? "" : "s"); | ||
| 1756 | fflush(stdout); | ||
| 1757 | return nFail == 0 ? 0 : 1; | ||
| 1758 | } | ||
| 1759 | |||
| 704 | static void ClearWalletRuntimeForTest() | 1760 | static void ClearWalletRuntimeForTest() |
| 705 | { | 1761 | { |
| 706 | CRITICAL_BLOCK(cs_mapKeys) | 1762 | CRITICAL_BLOCK(cs_mapKeys) |
| @@ -891,6 +1947,90 @@ static bool WriteTextFile(const string& strPath, const string& strText) | |||
| 891 | return fOk; | 1947 | return fOk; |
| 892 | } | 1948 | } |
| 893 | 1949 | ||
| 1950 | static bool MakeDirLocal(const string& strPath) | ||
| 1951 | { | ||
| 1952 | #ifdef _WIN32 | ||
| 1953 | return CreateDirectoryA(strPath.c_str(), NULL) || GetLastError() == ERROR_ALREADY_EXISTS; | ||
| 1954 | #else | ||
| 1955 | return mkdir(strPath.c_str(), 0700) == 0 || errno == EEXIST; | ||
| 1956 | #endif | ||
| 1957 | } | ||
| 1958 | |||
| 1959 | static bool CopyFileLocal(const string& strSrc, const string& strDst) | ||
| 1960 | { | ||
| 1961 | FILE* in = fopen(strSrc.c_str(), "rb"); | ||
| 1962 | if (!in) | ||
| 1963 | return false; | ||
| 1964 | FILE* out = fopen(strDst.c_str(), "wb"); | ||
| 1965 | if (!out) | ||
| 1966 | { | ||
| 1967 | fclose(in); | ||
| 1968 | return false; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | bool fOk = true; | ||
| 1972 | char buf[65536]; | ||
| 1973 | size_t n = 0; | ||
| 1974 | while ((n = fread(buf, 1, sizeof(buf), in)) > 0) | ||
| 1975 | { | ||
| 1976 | if (fwrite(buf, 1, n, out) != n) | ||
| 1977 | { | ||
| 1978 | fOk = false; | ||
| 1979 | break; | ||
| 1980 | } | ||
| 1981 | } | ||
| 1982 | if (ferror(in)) | ||
| 1983 | fOk = false; | ||
| 1984 | if (fclose(out) != 0) | ||
| 1985 | fOk = false; | ||
| 1986 | fclose(in); | ||
| 1987 | return fOk; | ||
| 1988 | } | ||
| 1989 | |||
| 1990 | static bool DirectoryHasFileWithPrefix(const string& strDir, const string& strPrefix) | ||
| 1991 | { | ||
| 1992 | #ifdef _WIN32 | ||
| 1993 | WIN32_FIND_DATAA findData; | ||
| 1994 | string pattern = strDir + "\\*"; | ||
| 1995 | HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); | ||
| 1996 | if (hFind == INVALID_HANDLE_VALUE) | ||
| 1997 | return false; | ||
| 1998 | bool fFound = false; | ||
| 1999 | do | ||
| 2000 | { | ||
| 2001 | if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) | ||
| 2002 | { | ||
| 2003 | string name = findData.cFileName; | ||
| 2004 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 2005 | { | ||
| 2006 | fFound = true; | ||
| 2007 | break; | ||
| 2008 | } | ||
| 2009 | } | ||
| 2010 | } | ||
| 2011 | while (FindNextFileA(hFind, &findData)); | ||
| 2012 | FindClose(hFind); | ||
| 2013 | return fFound; | ||
| 2014 | #else | ||
| 2015 | DIR* dir = opendir(strDir.c_str()); | ||
| 2016 | if (!dir) | ||
| 2017 | return false; | ||
| 2018 | bool fFound = false; | ||
| 2019 | struct dirent* ent; | ||
| 2020 | while ((ent = readdir(dir)) != NULL) | ||
| 2021 | { | ||
| 2022 | string name = ent->d_name; | ||
| 2023 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 2024 | { | ||
| 2025 | fFound = true; | ||
| 2026 | break; | ||
| 2027 | } | ||
| 2028 | } | ||
| 2029 | closedir(dir); | ||
| 2030 | return fFound; | ||
| 2031 | #endif | ||
| 2032 | } | ||
| 2033 | |||
| 894 | static string QuoteCommandArg(const string& str) | 2034 | static string QuoteCommandArg(const string& str) |
| 895 | { | 2035 | { |
| 896 | string out = "\""; | 2036 | string out = "\""; |
| @@ -907,7 +2047,8 @@ static string QuoteCommandArg(const string& str) | |||
| 907 | 2047 | ||
| 908 | static int RunBitflashChild(const string& strExe, | 2048 | static int RunBitflashChild(const string& strExe, |
| 909 | const vector<string>& vArgs, | 2049 | const vector<string>& vArgs, |
| 910 | const string* pStdinFile = NULL) | 2050 | const string* pStdinFile = NULL, |
| 2051 | const string* pOutputFile = NULL) | ||
| 911 | { | 2052 | { |
| 912 | #ifdef _WIN32 | 2053 | #ifdef _WIN32 |
| 913 | vector<char*> argv; | 2054 | vector<char*> argv; |
| @@ -920,8 +2061,16 @@ static int RunBitflashChild(const string& strExe, | |||
| 920 | int nOldErr = _dup(2); | 2061 | int nOldErr = _dup(2); |
| 921 | int nOldIn = _dup(0); | 2062 | int nOldIn = _dup(0); |
| 922 | int nNull = _open("NUL", _O_WRONLY); | 2063 | int nNull = _open("NUL", _O_WRONLY); |
| 2064 | int nOut = -1; | ||
| 923 | int nIn = -1; | 2065 | int nIn = -1; |
| 924 | if (nNull >= 0) | 2066 | if (pOutputFile) |
| 2067 | nOut = _open(pOutputFile->c_str(), _O_WRONLY|_O_CREAT|_O_TRUNC|_O_BINARY, _S_IREAD|_S_IWRITE); | ||
| 2068 | if (nOut >= 0) | ||
| 2069 | { | ||
| 2070 | _dup2(nOut, 1); | ||
| 2071 | _dup2(nOut, 2); | ||
| 2072 | } | ||
| 2073 | else if (nNull >= 0) | ||
| 925 | { | 2074 | { |
| 926 | _dup2(nNull, 1); | 2075 | _dup2(nNull, 1); |
| 927 | _dup2(nNull, 2); | 2076 | _dup2(nNull, 2); |
| @@ -935,6 +2084,8 @@ static int RunBitflashChild(const string& strExe, | |||
| 935 | int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); | 2084 | int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); |
| 936 | if (nIn >= 0) | 2085 | if (nIn >= 0) |
| 937 | _close(nIn); | 2086 | _close(nIn); |
| 2087 | if (nOut >= 0) | ||
| 2088 | _close(nOut); | ||
| 938 | if (nNull >= 0) | 2089 | if (nNull >= 0) |
| 939 | _close(nNull); | 2090 | _close(nNull); |
| 940 | if (nOldIn >= 0) | 2091 | if (nOldIn >= 0) |
| @@ -959,8 +2110,17 @@ static int RunBitflashChild(const string& strExe, | |||
| 959 | strCmd += " " + QuoteCommandArg(vArgs[i]); | 2110 | strCmd += " " + QuoteCommandArg(vArgs[i]); |
| 960 | if (pStdinFile) | 2111 | if (pStdinFile) |
| 961 | strCmd += " < " + QuoteCommandArg(*pStdinFile); | 2112 | strCmd += " < " + QuoteCommandArg(*pStdinFile); |
| 962 | strCmd += " > /dev/null 2>&1"; | 2113 | if (pOutputFile) |
| 963 | return system(strCmd.c_str()); | 2114 | strCmd += " > " + QuoteCommandArg(*pOutputFile) + " 2>&1"; |
| 2115 | else | ||
| 2116 | strCmd += " > /dev/null 2>&1"; | ||
| 2117 | // system() returns a wait status, not the exit code; unwrap it so callers | ||
| 2118 | // see the same value the Windows _spawnv path returns (e.g. exit code 2 is | ||
| 2119 | // 2, not 512). Without this every "== 2" assertion fails only on Linux. | ||
| 2120 | int nSysRet = system(strCmd.c_str()); | ||
| 2121 | if (nSysRet != -1 && WIFEXITED(nSysRet)) | ||
| 2122 | return WEXITSTATUS(nSysRet); | ||
| 2123 | return nSysRet; | ||
| 964 | #endif | 2124 | #endif |
| 965 | } | 2125 | } |
| 966 | 2126 | ||
| @@ -1001,6 +2161,13 @@ static int RunWalletEncryptSelfTest() | |||
| 1001 | TopUpKeyPool(); | 2161 | TopUpKeyPool(); |
| 1002 | 2162 | ||
| 1003 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | 2163 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); |
| 2164 | CKey publicLabelKey; | ||
| 2165 | publicLabelKey.MakeNewKey(); | ||
| 2166 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2167 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2168 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2169 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2170 | |||
| 1004 | CPrivKey vchDefaultPrivKey; | 2171 | CPrivKey vchDefaultPrivKey; |
| 1005 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | 2172 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), |
| 1006 | "the default private key is readable before encryption") ? 0 : 1; | 2173 | "the default private key is readable before encryption") ? 0 : 1; |
| @@ -1028,16 +2195,31 @@ static int RunWalletEncryptSelfTest() | |||
| 1028 | #else | 2195 | #else |
| 1029 | string strExe = cwd + "/bitflash-node"; | 2196 | string strExe = cwd + "/bitflash-node"; |
| 1030 | #endif | 2197 | #endif |
| 2198 | string strStorageAudit = tmp + "/encrypted-storage-audit.txt"; | ||
| 1031 | string strWrongDump = tmp + "/wrong-pass-dump.txt"; | 2199 | string strWrongDump = tmp + "/wrong-pass-dump.txt"; |
| 1032 | string strRightDump = tmp + "/right-pass-dump.txt"; | 2200 | string strRightDump = tmp + "/right-pass-dump.txt"; |
| 1033 | string strLiteralDump = tmp + "/literal-pass-dump.txt"; | 2201 | string strLiteralDump = tmp + "/literal-pass-dump.txt"; |
| 1034 | string strStdinDump = tmp + "/stdin-pass-dump.txt"; | 2202 | string strStdinDump = tmp + "/stdin-pass-dump.txt"; |
| 2203 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2204 | string strUnlockedAudit = tmp + "/unlocked-recovery-audit.txt"; | ||
| 1035 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | 2205 | string strWrongPassFile = tmp + "/wrong-pass.txt"; |
| 1036 | string strRightPassFile = tmp + "/right-pass.txt"; | 2206 | string strRightPassFile = tmp + "/right-pass.txt"; |
| 1037 | nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && | 2207 | nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && |
| 1038 | WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), | 2208 | WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), |
| 1039 | "passphrase files can be written for restarted commands") ? 0 : 1; | 2209 | "passphrase files can be written for restarted commands") ? 0 : 1; |
| 1040 | 2210 | ||
| 2211 | vector<string> vStorageAuditArgs; | ||
| 2212 | vStorageAuditArgs.push_back("-datadir=" + tmp); | ||
| 2213 | vStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2214 | vStorageAuditArgs.push_back("-nogui"); | ||
| 2215 | int nStorageAuditRet = RunBitflashChild(strExe, vStorageAuditArgs, NULL, &strStorageAudit); | ||
| 2216 | nFail += Check(nStorageAuditRet == 0 && | ||
| 2217 | FileContainsText(strStorageAudit, "address book labels: 3"), | ||
| 2218 | "the encrypted rewrite keeps address-book records") ? 0 : 1; | ||
| 2219 | nFail += Check(nStorageAuditRet == 0 && | ||
| 2220 | FileContainsText(strStorageAudit, "settings: 1"), | ||
| 2221 | "the encrypted rewrite keeps wallet setting records") ? 0 : 1; | ||
| 2222 | |||
| 1041 | vector<string> vWrongArgs; | 2223 | vector<string> vWrongArgs; |
| 1042 | vWrongArgs.push_back("-datadir=" + tmp); | 2224 | vWrongArgs.push_back("-datadir=" + tmp); |
| 1043 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | 2225 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); |
| @@ -1047,6 +2229,17 @@ static int RunWalletEncryptSelfTest() | |||
| 1047 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | 2229 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), |
| 1048 | "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; | 2230 | "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; |
| 1049 | 2231 | ||
| 2232 | vector<string> vLockedAuditArgs; | ||
| 2233 | vLockedAuditArgs.push_back("-datadir=" + tmp); | ||
| 2234 | vLockedAuditArgs.push_back("-recoveryaudit"); | ||
| 2235 | vLockedAuditArgs.push_back("-nogui"); | ||
| 2236 | int nLockedAuditRet = RunBitflashChild(strExe, vLockedAuditArgs, NULL, &strLockedAudit); | ||
| 2237 | nFail += Check(nLockedAuditRet == 2 && | ||
| 2238 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit") && | ||
| 2239 | FileContainsText(strLockedAudit, "recovery coverage: unavailable while wallet is locked") && | ||
| 2240 | FileContainsText(strLockedAudit, "Unlock the wallet with /walletpassphrase"), | ||
| 2241 | "a locked encrypted wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2242 | |||
| 1050 | vector<string> vLiteralArgs; | 2243 | vector<string> vLiteralArgs; |
| 1051 | vLiteralArgs.push_back("-datadir=" + tmp); | 2244 | vLiteralArgs.push_back("-datadir=" + tmp); |
| 1052 | vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); | 2245 | vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); |
| @@ -1067,6 +2260,17 @@ static int RunWalletEncryptSelfTest() | |||
| 1067 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | 2260 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), |
| 1068 | "the restarted wallet can decrypt and dump the original key") ? 0 : 1; | 2261 | "the restarted wallet can decrypt and dump the original key") ? 0 : 1; |
| 1069 | 2262 | ||
| 2263 | vector<string> vUnlockedAuditArgs; | ||
| 2264 | vUnlockedAuditArgs.push_back("-datadir=" + tmp); | ||
| 2265 | vUnlockedAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2266 | vUnlockedAuditArgs.push_back("-recoveryaudit"); | ||
| 2267 | vUnlockedAuditArgs.push_back("-nogui"); | ||
| 2268 | int nUnlockedAuditRet = RunBitflashChild(strExe, vUnlockedAuditArgs, NULL, &strUnlockedAudit); | ||
| 2269 | nFail += Check(nUnlockedAuditRet == 0 && | ||
| 2270 | FileContainsText(strUnlockedAudit, "recovery phrase: present") && | ||
| 2271 | !FileContainsText(strUnlockedAudit, "recovery phrase: not installed"), | ||
| 2272 | "an unlocked encrypted wallet audits the installed recovery phrase") ? 0 : 1; | ||
| 2273 | |||
| 1070 | vector<string> vStdinArgs; | 2274 | vector<string> vStdinArgs; |
| 1071 | vStdinArgs.push_back("-datadir=" + tmp); | 2275 | vStdinArgs.push_back("-datadir=" + tmp); |
| 1072 | vStdinArgs.push_back("-walletpassphrase"); | 2276 | vStdinArgs.push_back("-walletpassphrase"); |
| @@ -1097,6 +2301,737 @@ static int RunWalletEncryptSelfTest() | |||
| 1097 | return nFail == 0 ? 0 : 1; | 2301 | return nFail == 0 ? 0 : 1; |
| 1098 | } | 2302 | } |
| 1099 | 2303 | ||
| 2304 | // Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley | ||
| 2305 | // DB encrypt test above, this drives the real command-line path end to end in | ||
| 2306 | // child processes: build a Berkeley DB wallet, export it to SQLite, then run | ||
| 2307 | // -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out | ||
| 2308 | // encrypted, unlockable, and with wallet.dat left untouched. | ||
| 2309 | static int RunWalletSQLiteEncryptSelfTest() | ||
| 2310 | { | ||
| 2311 | fflush(stdout); | ||
| 2312 | printf("wallet-sqlite-encrypt self-test\n"); | ||
| 2313 | |||
| 2314 | std::string tmp; | ||
| 2315 | std::string cwd; | ||
| 2316 | if (!MakeTempDir(tmp)) | ||
| 2317 | { | ||
| 2318 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2319 | return 1; | ||
| 2320 | } | ||
| 2321 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2322 | { | ||
| 2323 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2324 | RemoveTree(tmp); | ||
| 2325 | return 1; | ||
| 2326 | } | ||
| 2327 | |||
| 2328 | int nFail = 0; | ||
| 2329 | strSetDataDir = tmp; | ||
| 2330 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2331 | |||
| 2332 | try | ||
| 2333 | { | ||
| 2334 | // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, | ||
| 2335 | // and a couple of public records, then release it. Everything after this | ||
| 2336 | // runs the real command-line paths in child processes. | ||
| 2337 | if (!LoadWallet()) | ||
| 2338 | throw std::runtime_error("LoadWallet failed"); | ||
| 2339 | |||
| 2340 | string strError; | ||
| 2341 | const string strMnemonic = | ||
| 2342 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; | ||
| 2343 | nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), | ||
| 2344 | "a phrase can be installed before encryption") ? 0 : 1; | ||
| 2345 | TopUpKeyPool(); | ||
| 2346 | |||
| 2347 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2348 | CKey publicLabelKey; | ||
| 2349 | publicLabelKey.MakeNewKey(); | ||
| 2350 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2351 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2352 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2353 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2354 | |||
| 2355 | CPrivKey vchDefaultPrivKey; | ||
| 2356 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2357 | "the default private key is readable before encryption") ? 0 : 1; | ||
| 2358 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); | ||
| 2359 | vector<unsigned char> vchPlainHDMaster = vchHDMaster; | ||
| 2360 | vector<unsigned char> vchPlainHDChainCode = vchHDChainCode; | ||
| 2361 | |||
| 2362 | // Release wallet.dat so a child process can read it for the export. | ||
| 2363 | DBFlush(true); | ||
| 2364 | |||
| 2365 | #ifdef _WIN32 | ||
| 2366 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2367 | #else | ||
| 2368 | string strExe = cwd + "/bitflash-node"; | ||
| 2369 | #endif | ||
| 2370 | string strSQLitePath = tmp + "/wallet.sqlite"; | ||
| 2371 | string strWalletDatPath = tmp + "/wallet.dat"; | ||
| 2372 | string strRightPassFile = tmp + "/right-pass.txt"; | ||
| 2373 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | ||
| 2374 | nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && | ||
| 2375 | WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), | ||
| 2376 | "passphrase files can be written for restarted commands") ? 0 : 1; | ||
| 2377 | |||
| 2378 | // Step 1: export the Berkeley DB wallet to SQLite. | ||
| 2379 | vector<string> vExportArgs; | ||
| 2380 | vExportArgs.push_back("-datadir=" + tmp); | ||
| 2381 | vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); | ||
| 2382 | vExportArgs.push_back("-nogui"); | ||
| 2383 | int nExportRet = RunBitflashChild(strExe, vExportArgs); | ||
| 2384 | nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), | ||
| 2385 | "the wallet exports to a SQLite file") ? 0 : 1; | ||
| 2386 | nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2387 | "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; | ||
| 2388 | |||
| 2389 | // Step 2: encrypt through the real SQLite backend command path. | ||
| 2390 | string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; | ||
| 2391 | vector<string> vEncryptArgs; | ||
| 2392 | vEncryptArgs.push_back("-datadir=" + tmp); | ||
| 2393 | vEncryptArgs.push_back("-walletbackend=sqlite"); | ||
| 2394 | vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); | ||
| 2395 | vEncryptArgs.push_back("-nogui"); | ||
| 2396 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2397 | nFail += Check(nEncryptRet == 0, | ||
| 2398 | "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; | ||
| 2399 | nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), | ||
| 2400 | "encryption took the SQLite persistence path") ? 0 : 1; | ||
| 2401 | nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), | ||
| 2402 | "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; | ||
| 2403 | |||
| 2404 | // The encrypted SQLite wallet must no longer hold the plaintext secrets. | ||
| 2405 | nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2406 | "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; | ||
| 2407 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), | ||
| 2408 | "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; | ||
| 2409 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), | ||
| 2410 | "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; | ||
| 2411 | |||
| 2412 | // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path | ||
| 2413 | // must never touch it, so its plaintext key is still exactly there. | ||
| 2414 | nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), | ||
| 2415 | "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; | ||
| 2416 | |||
| 2417 | // Step 3: a locked encrypted SQLite wallet reports that phrase coverage | ||
| 2418 | // needs an unlock, exactly as the Berkeley DB path does (exit code 2). | ||
| 2419 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2420 | vector<string> vLockedArgs; | ||
| 2421 | vLockedArgs.push_back("-datadir=" + tmp); | ||
| 2422 | vLockedArgs.push_back("-walletbackend=sqlite"); | ||
| 2423 | vLockedArgs.push_back("-recoveryaudit"); | ||
| 2424 | vLockedArgs.push_back("-nogui"); | ||
| 2425 | int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); | ||
| 2426 | nFail += Check(nLockedRet == 2 && | ||
| 2427 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), | ||
| 2428 | "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2429 | |||
| 2430 | // The wrong passphrase is rejected and reveals nothing. | ||
| 2431 | string strWrongDump = tmp + "/wrong-dump.txt"; | ||
| 2432 | vector<string> vWrongArgs; | ||
| 2433 | vWrongArgs.push_back("-datadir=" + tmp); | ||
| 2434 | vWrongArgs.push_back("-walletbackend=sqlite"); | ||
| 2435 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | ||
| 2436 | vWrongArgs.push_back("-dumpwallet=" + strWrongDump); | ||
| 2437 | vWrongArgs.push_back("-nogui"); | ||
| 2438 | int nWrongRet = RunBitflashChild(strExe, vWrongArgs); | ||
| 2439 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | ||
| 2440 | "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; | ||
| 2441 | |||
| 2442 | // The right passphrase unlocks and can dump the original key. | ||
| 2443 | string strRightDump = tmp + "/right-dump.txt"; | ||
| 2444 | vector<string> vRightArgs; | ||
| 2445 | vRightArgs.push_back("-datadir=" + tmp); | ||
| 2446 | vRightArgs.push_back("-walletbackend=sqlite"); | ||
| 2447 | vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2448 | vRightArgs.push_back("-dumpwallet=" + strRightDump); | ||
| 2449 | vRightArgs.push_back("-nogui"); | ||
| 2450 | int nRightRet = RunBitflashChild(strExe, vRightArgs); | ||
| 2451 | nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), | ||
| 2452 | "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; | ||
| 2453 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | ||
| 2454 | "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; | ||
| 2455 | |||
| 2456 | // The recovery phrase survives the encryption. | ||
| 2457 | string strAudit = tmp + "/recovery-audit.txt"; | ||
| 2458 | vector<string> vAuditArgs; | ||
| 2459 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2460 | vAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2461 | vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2462 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2463 | vAuditArgs.push_back("-nogui"); | ||
| 2464 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2465 | nFail += Check(nAuditRet == 0 && | ||
| 2466 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2467 | "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; | ||
| 2468 | |||
| 2469 | // One-shot storage commands honour -walletbackend. The SQLite wallet is | ||
| 2470 | // encrypted; the wallet.dat beside it is still plaintext. Same datadir, | ||
| 2471 | // opposite answers -> the flag routes the diagnostic to the right file. | ||
| 2472 | string strSqliteAuditJson = tmp + "/sqlite-audit.json"; | ||
| 2473 | vector<string> vSqliteAuditArgs; | ||
| 2474 | vSqliteAuditArgs.push_back("-datadir=" + tmp); | ||
| 2475 | vSqliteAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2476 | vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson); | ||
| 2477 | vSqliteAuditArgs.push_back("-nogui"); | ||
| 2478 | int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs); | ||
| 2479 | nFail += Check(nSqliteAuditRet == 0 && | ||
| 2480 | FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") && | ||
| 2481 | FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""), | ||
| 2482 | "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2483 | |||
| 2484 | string strBdbAuditJson = tmp + "/bdb-audit.json"; | ||
| 2485 | vector<string> vBdbAuditArgs; | ||
| 2486 | vBdbAuditArgs.push_back("-datadir=" + tmp); | ||
| 2487 | vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson); | ||
| 2488 | vBdbAuditArgs.push_back("-nogui"); | ||
| 2489 | int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs); | ||
| 2490 | nFail += Check(nBdbAuditRet == 0 && | ||
| 2491 | FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") && | ||
| 2492 | FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""), | ||
| 2493 | "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1; | ||
| 2494 | |||
| 2495 | string strSqliteCheckOut = tmp + "/sqlite-check.txt"; | ||
| 2496 | vector<string> vSqliteCheckArgs; | ||
| 2497 | vSqliteCheckArgs.push_back("-datadir=" + tmp); | ||
| 2498 | vSqliteCheckArgs.push_back("-walletbackend=sqlite"); | ||
| 2499 | vSqliteCheckArgs.push_back("-walletstoragecheck"); | ||
| 2500 | vSqliteCheckArgs.push_back("-nogui"); | ||
| 2501 | int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut); | ||
| 2502 | nFail += Check(nSqliteCheckRet == 0, | ||
| 2503 | "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2504 | |||
| 2505 | // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat. | ||
| 2506 | // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds | ||
| 2507 | // none of the plaintext key that wallet.dat still contains. | ||
| 2508 | string strBackupDest = tmp + "/backup.sqlite"; | ||
| 2509 | vector<string> vBackupArgs; | ||
| 2510 | vBackupArgs.push_back("-datadir=" + tmp); | ||
| 2511 | vBackupArgs.push_back("-walletbackend=sqlite"); | ||
| 2512 | vBackupArgs.push_back("-backupwallet=" + strBackupDest); | ||
| 2513 | vBackupArgs.push_back("-nogui"); | ||
| 2514 | int nBackupRet = RunBitflashChild(strExe, vBackupArgs); | ||
| 2515 | nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()), | ||
| 2516 | "-backupwallet under SQLite writes a backup file") ? 0 : 1; | ||
| 2517 | nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes), | ||
| 2518 | "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1; | ||
| 2519 | |||
| 2520 | string strBackupLoadCheck = tmp + "/backup-loadcheck.txt"; | ||
| 2521 | vector<string> vLoadCheckArgs; | ||
| 2522 | vLoadCheckArgs.push_back("-datadir=" + tmp); | ||
| 2523 | vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest); | ||
| 2524 | vLoadCheckArgs.push_back("-nogui"); | ||
| 2525 | int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck); | ||
| 2526 | nFail += Check(nBackupLoadRet == 0, | ||
| 2527 | "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1; | ||
| 2528 | } | ||
| 2529 | catch (const std::exception& e) | ||
| 2530 | { | ||
| 2531 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2532 | nFail++; | ||
| 2533 | } | ||
| 2534 | catch (...) | ||
| 2535 | { | ||
| 2536 | printf(" FAIL unknown exception\n"); | ||
| 2537 | nFail++; | ||
| 2538 | } | ||
| 2539 | |||
| 2540 | DBFlush(true); | ||
| 2541 | SetCurrentDir(cwd); | ||
| 2542 | RemoveTree(tmp); | ||
| 2543 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2544 | nFail, nFail == 1 ? "" : "s"); | ||
| 2545 | fflush(stdout); | ||
| 2546 | return nFail == 0 ? 0 : 1; | ||
| 2547 | } | ||
| 2548 | |||
| 2549 | // Backend resolution when no -walletbackend flag is given: a fresh install | ||
| 2550 | // starts on SQLite (the new default), an existing wallet.dat stays on Berkeley | ||
| 2551 | // DB, the persistent marker overrides the default, and an explicit flag | ||
| 2552 | // overrides everything. Each scenario runs a real child process. | ||
| 2553 | static int RunWalletBackendDefaultSelfTest() | ||
| 2554 | { | ||
| 2555 | fflush(stdout); | ||
| 2556 | printf("wallet-backend-default self-test\n"); | ||
| 2557 | |||
| 2558 | std::string tmp, cwd; | ||
| 2559 | if (!MakeTempDir(tmp)) | ||
| 2560 | { | ||
| 2561 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2562 | return 1; | ||
| 2563 | } | ||
| 2564 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2565 | { | ||
| 2566 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2567 | RemoveTree(tmp); | ||
| 2568 | return 1; | ||
| 2569 | } | ||
| 2570 | |||
| 2571 | int nFail = 0; | ||
| 2572 | strSetDataDir = tmp; | ||
| 2573 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2574 | |||
| 2575 | #ifdef _WIN32 | ||
| 2576 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2577 | #else | ||
| 2578 | string strExe = cwd + "/bitflash-node"; | ||
| 2579 | #endif | ||
| 2580 | |||
| 2581 | try | ||
| 2582 | { | ||
| 2583 | // 1. Fresh datadir, no flag -> this fork refuses, where upstream creates. | ||
| 2584 | // An empty datadir on a headless node is far more often a mount or | ||
| 2585 | // backup fault than a first run, and creating a wallet also pins the | ||
| 2586 | // backend marker, after which restoring the real wallet is a no-op. | ||
| 2587 | // Nothing may be left behind: no wallet of either kind, no marker. | ||
| 2588 | string d1 = tmp + "/fresh"; | ||
| 2589 | nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; | ||
| 2590 | vector<string> a1; | ||
| 2591 | a1.push_back("-datadir=" + d1); | ||
| 2592 | a1.push_back("-nomanagedtor"); | ||
| 2593 | a1.push_back("-nogui"); | ||
| 2594 | a1.push_back("-newaddress"); | ||
| 2595 | int r0 = RunBitflashChild(strExe, a1); | ||
| 2596 | nFail += Check(r0 != 0 && | ||
| 2597 | !FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2598 | !FileExists((d1 + "/wallet.dat").c_str()) && | ||
| 2599 | !FileExists((d1 + "/wallet-backend").c_str()), | ||
| 2600 | "a flagless headless start refuses to create a wallet") ? 0 : 1; | ||
| 2601 | |||
| 2602 | // 1a. -createwallet says it was meant: new wallet.sqlite, marker | ||
| 2603 | // "sqlite", no wallet.dat. | ||
| 2604 | vector<string> a1c = a1; | ||
| 2605 | a1c.push_back("-createwallet"); | ||
| 2606 | int r1 = RunBitflashChild(strExe, a1c); | ||
| 2607 | nFail += Check(r1 == 0 && | ||
| 2608 | FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2609 | !FileExists((d1 + "/wallet.dat").c_str()) && | ||
| 2610 | FileContainsText(d1 + "/wallet-backend", "sqlite"), | ||
| 2611 | "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1; | ||
| 2612 | |||
| 2613 | // 1b. Second run reuses it without the flag -- the refusal only guards | ||
| 2614 | // creation, so an existing wallet still starts flagless. | ||
| 2615 | int r1b = RunBitflashChild(strExe, a1); | ||
| 2616 | nFail += Check(r1b == 0 && | ||
| 2617 | FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2618 | !FileExists((d1 + "/wallet.dat").c_str()), | ||
| 2619 | "the recorded SQLite choice survives a flagless restart") ? 0 : 1; | ||
| 2620 | |||
| 2621 | // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. | ||
| 2622 | string d2 = tmp + "/existing-bdb"; | ||
| 2623 | nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1; | ||
| 2624 | vector<string> a2c; | ||
| 2625 | a2c.push_back("-datadir=" + d2); | ||
| 2626 | a2c.push_back("-nomanagedtor"); | ||
| 2627 | a2c.push_back("-nogui"); | ||
| 2628 | a2c.push_back("-walletbackend=bdb"); | ||
| 2629 | a2c.push_back("-newphrase"); | ||
| 2630 | RunBitflashChild(strExe, a2c); | ||
| 2631 | vector<string> a2; | ||
| 2632 | a2.push_back("-datadir=" + d2); | ||
| 2633 | a2.push_back("-nomanagedtor"); | ||
| 2634 | a2.push_back("-nogui"); | ||
| 2635 | a2.push_back("-newaddress"); | ||
| 2636 | int r2 = RunBitflashChild(strExe, a2); | ||
| 2637 | nFail += Check(r2 == 0 && | ||
| 2638 | FileExists((d2 + "/wallet.dat").c_str()) && | ||
| 2639 | !FileExists((d2 + "/wallet.sqlite").c_str()), | ||
| 2640 | "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1; | ||
| 2641 | |||
| 2642 | // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir. | ||
| 2643 | string d3 = tmp + "/marker-bdb"; | ||
| 2644 | nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1; | ||
| 2645 | nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"), | ||
| 2646 | "a bdb marker can be written") ? 0 : 1; | ||
| 2647 | vector<string> a3; | ||
| 2648 | a3.push_back("-datadir=" + d3); | ||
| 2649 | a3.push_back("-nomanagedtor"); | ||
| 2650 | a3.push_back("-nogui"); | ||
| 2651 | a3.push_back("-newaddress"); | ||
| 2652 | int r3 = RunBitflashChild(strExe, a3); | ||
| 2653 | nFail += Check(r3 == 0 && | ||
| 2654 | FileExists((d3 + "/wallet.dat").c_str()) && | ||
| 2655 | !FileExists((d3 + "/wallet.sqlite").c_str()), | ||
| 2656 | "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1; | ||
| 2657 | |||
| 2658 | // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat. | ||
| 2659 | string d4 = tmp + "/explicit-bdb"; | ||
| 2660 | nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1; | ||
| 2661 | vector<string> a4; | ||
| 2662 | a4.push_back("-datadir=" + d4); | ||
| 2663 | a4.push_back("-nomanagedtor"); | ||
| 2664 | a4.push_back("-nogui"); | ||
| 2665 | a4.push_back("-walletbackend=bdb"); | ||
| 2666 | a4.push_back("-newaddress"); | ||
| 2667 | int r4 = RunBitflashChild(strExe, a4); | ||
| 2668 | nFail += Check(r4 == 0 && | ||
| 2669 | FileExists((d4 + "/wallet.dat").c_str()) && | ||
| 2670 | !FileExists((d4 + "/wallet.sqlite").c_str()), | ||
| 2671 | "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1; | ||
| 2672 | } | ||
| 2673 | catch (const std::exception& e) | ||
| 2674 | { | ||
| 2675 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2676 | nFail++; | ||
| 2677 | } | ||
| 2678 | catch (...) | ||
| 2679 | { | ||
| 2680 | printf(" FAIL unknown exception\n"); | ||
| 2681 | nFail++; | ||
| 2682 | } | ||
| 2683 | |||
| 2684 | DBFlush(true); | ||
| 2685 | SetCurrentDir(cwd); | ||
| 2686 | RemoveTree(tmp); | ||
| 2687 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2688 | nFail, nFail == 1 ? "" : "s"); | ||
| 2689 | fflush(stdout); | ||
| 2690 | return nFail == 0 ? 0 : 1; | ||
| 2691 | } | ||
| 2692 | |||
| 2693 | // The wizard's conversion action, headless: build a Berkeley DB wallet, run | ||
| 2694 | // DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a | ||
| 2695 | // usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then | ||
| 2696 | // that a flagless restart opens the converted SQLite wallet. | ||
| 2697 | static int RunWalletConvertSelfTest() | ||
| 2698 | { | ||
| 2699 | fflush(stdout); | ||
| 2700 | printf("wallet-convert self-test\n"); | ||
| 2701 | |||
| 2702 | std::string tmp, cwd; | ||
| 2703 | if (!MakeTempDir(tmp)) | ||
| 2704 | { | ||
| 2705 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2706 | return 1; | ||
| 2707 | } | ||
| 2708 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2709 | { | ||
| 2710 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2711 | RemoveTree(tmp); | ||
| 2712 | return 1; | ||
| 2713 | } | ||
| 2714 | |||
| 2715 | int nFail = 0; | ||
| 2716 | strSetDataDir = tmp; | ||
| 2717 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2718 | |||
| 2719 | #ifdef _WIN32 | ||
| 2720 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2721 | #else | ||
| 2722 | string strExe = cwd + "/bitflash-node"; | ||
| 2723 | #endif | ||
| 2724 | |||
| 2725 | try | ||
| 2726 | { | ||
| 2727 | if (!LoadWallet()) | ||
| 2728 | throw std::runtime_error("LoadWallet failed"); | ||
| 2729 | |||
| 2730 | string strError; | ||
| 2731 | nFail += Check(SetHDSeedFromMnemonic( | ||
| 2732 | "abandon abandon abandon abandon abandon abandon abandon " | ||
| 2733 | "abandon abandon abandon abandon about", strError), | ||
| 2734 | "a phrase can be installed before conversion") ? 0 : 1; | ||
| 2735 | TopUpKeyPool(); | ||
| 2736 | |||
| 2737 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2738 | CPrivKey vchDefaultPrivKey; | ||
| 2739 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2740 | "the default private key is readable before conversion") ? 0 : 1; | ||
| 2741 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), | ||
| 2742 | vchDefaultPrivKey.end()); | ||
| 2743 | |||
| 2744 | // The conversion action the GUI button invokes. | ||
| 2745 | int nCopied = 0; | ||
| 2746 | string strConvertError; | ||
| 2747 | bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); | ||
| 2748 | nFail += Check(fConverted && nCopied > 0, | ||
| 2749 | "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; | ||
| 2750 | nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && | ||
| 2751 | FileContainsText(tmp + "/wallet-backend", "sqlite"), | ||
| 2752 | "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; | ||
| 2753 | nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), | ||
| 2754 | "wallet.dat is left untouched by the conversion") ? 0 : 1; | ||
| 2755 | |||
| 2756 | // Idempotence guard: a second conversion refuses rather than overwrite. | ||
| 2757 | int nCopied2 = 0; | ||
| 2758 | string strSecondError; | ||
| 2759 | nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), | ||
| 2760 | "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; | ||
| 2761 | |||
| 2762 | DBFlush(true); | ||
| 2763 | |||
| 2764 | // A flagless restart now opens the converted SQLite wallet (the marker | ||
| 2765 | // routes it) and finds the same recovery phrase. | ||
| 2766 | string strAudit = tmp + "/converted-recovery-audit.txt"; | ||
| 2767 | vector<string> vAuditArgs; | ||
| 2768 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2769 | vAuditArgs.push_back("-nomanagedtor"); | ||
| 2770 | vAuditArgs.push_back("-nogui"); | ||
| 2771 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2772 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2773 | nFail += Check(nAuditRet == 0 && | ||
| 2774 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2775 | "a flagless restart opens the converted SQLite wallet") ? 0 : 1; | ||
| 2776 | } | ||
| 2777 | catch (const std::exception& e) | ||
| 2778 | { | ||
| 2779 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2780 | nFail++; | ||
| 2781 | } | ||
| 2782 | catch (...) | ||
| 2783 | { | ||
| 2784 | printf(" FAIL unknown exception\n"); | ||
| 2785 | nFail++; | ||
| 2786 | } | ||
| 2787 | |||
| 2788 | DBFlush(true); | ||
| 2789 | SetCurrentDir(cwd); | ||
| 2790 | RemoveTree(tmp); | ||
| 2791 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2792 | nFail, nFail == 1 ? "" : "s"); | ||
| 2793 | fflush(stdout); | ||
| 2794 | return nFail == 0 ? 0 : 1; | ||
| 2795 | } | ||
| 2796 | |||
| 2797 | static int RunWalletPortabilitySelfTest() | ||
| 2798 | { | ||
| 2799 | fflush(stdout); | ||
| 2800 | printf("wallet-portability self-test\n"); | ||
| 2801 | |||
| 2802 | std::string tmp; | ||
| 2803 | std::string cwd; | ||
| 2804 | if (!MakeTempDir(tmp)) | ||
| 2805 | { | ||
| 2806 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2807 | return 1; | ||
| 2808 | } | ||
| 2809 | if (!GetCurrentDir(cwd)) | ||
| 2810 | { | ||
| 2811 | printf(" FAIL could not read current directory\n"); | ||
| 2812 | RemoveTree(tmp); | ||
| 2813 | return 1; | ||
| 2814 | } | ||
| 2815 | |||
| 2816 | int nFail = 0; | ||
| 2817 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 2818 | |||
| 2819 | try | ||
| 2820 | { | ||
| 2821 | #ifdef _WIN32 | ||
| 2822 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2823 | #else | ||
| 2824 | string strExe = cwd + "/bitflash-node"; | ||
| 2825 | #endif | ||
| 2826 | string strOriginal = tmp + "/original"; | ||
| 2827 | string strRawCopy = tmp + "/raw-copy"; | ||
| 2828 | string strBackupCopy = tmp + "/backup-copy"; | ||
| 2829 | nFail += Check(MakeDirLocal(strOriginal) && | ||
| 2830 | MakeDirLocal(strRawCopy) && | ||
| 2831 | MakeDirLocal(strBackupCopy), | ||
| 2832 | "temporary wallet directories can be created") ? 0 : 1; | ||
| 2833 | |||
| 2834 | string strCreateOut = tmp + "/create.txt"; | ||
| 2835 | vector<string> vCreateArgs; | ||
| 2836 | vCreateArgs.push_back("-datadir=" + strOriginal); | ||
| 2837 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 2838 | vCreateArgs.push_back("-nogui"); | ||
| 2839 | vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability | ||
| 2840 | vCreateArgs.push_back("-newphrase"); | ||
| 2841 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 2842 | nFail += Check(nCreateRet == 0 && | ||
| 2843 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 2844 | "a phrase wallet can be created in a fresh datadir") ? 0 : 1; | ||
| 2845 | |||
| 2846 | string strOriginalAudit = tmp + "/original-audit.txt"; | ||
| 2847 | vector<string> vOriginalAuditArgs; | ||
| 2848 | vOriginalAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2849 | vOriginalAuditArgs.push_back("-nomanagedtor"); | ||
| 2850 | vOriginalAuditArgs.push_back("-nogui"); | ||
| 2851 | vOriginalAuditArgs.push_back("-recoveryaudit"); | ||
| 2852 | int nOriginalAuditRet = RunBitflashChild(strExe, vOriginalAuditArgs, NULL, &strOriginalAudit); | ||
| 2853 | nFail += Check(nOriginalAuditRet == 0 && | ||
| 2854 | FileContainsText(strOriginalAudit, "recovery phrase: present") && | ||
| 2855 | FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"), | ||
| 2856 | "the original wallet reports its recovery phrase") ? 0 : 1; | ||
| 2857 | |||
| 2858 | string strOriginalStorageAudit = tmp + "/original-storage-audit.txt"; | ||
| 2859 | vector<string> vOriginalStorageAuditArgs; | ||
| 2860 | vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2861 | vOriginalStorageAuditArgs.push_back("-nomanagedtor"); | ||
| 2862 | vOriginalStorageAuditArgs.push_back("-nogui"); | ||
| 2863 | vOriginalStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2864 | int nOriginalStorageAuditRet = | ||
| 2865 | RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit); | ||
| 2866 | nFail += Check(nOriginalStorageAuditRet == 0 && | ||
| 2867 | FileContainsText(strOriginalStorageAudit, "Wallet storage audit") && | ||
| 2868 | FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") && | ||
| 2869 | FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") && | ||
| 2870 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), | ||
| 2871 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; | ||
| 2872 | |||
| 2873 | string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; | ||
| 2874 | vector<string> vOriginalStorageCheckArgs; | ||
| 2875 | vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 2876 | vOriginalStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 2877 | vOriginalStorageCheckArgs.push_back("-nogui"); | ||
| 2878 | vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 2879 | int nOriginalStorageCheckRet = | ||
| 2880 | RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); | ||
| 2881 | nFail += Check(nOriginalStorageCheckRet == 0 && | ||
| 2882 | FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && | ||
| 2883 | FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), | ||
| 2884 | "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; | ||
| 2885 | |||
| 2886 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; | ||
| 2887 | vector<string> vOriginalStorageAuditJsonArgs; | ||
| 2888 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | ||
| 2889 | vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor"); | ||
| 2890 | vOriginalStorageAuditJsonArgs.push_back("-nogui"); | ||
| 2891 | vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + | ||
| 2892 | strOriginalStorageAuditJson); | ||
| 2893 | int nOriginalStorageAuditJsonRet = | ||
| 2894 | RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs); | ||
| 2895 | nFail += Check(nOriginalStorageAuditJsonRet == 0 && | ||
| 2896 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2897 | "\"format\": \"bitflash-wallet-storage-audit-v1\"") && | ||
| 2898 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2899 | "\"plain_hd_seed\": \"complete\"") && | ||
| 2900 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2901 | "\"encrypted_hd_seed\": \"none\""), | ||
| 2902 | "the storage audit can write plaintext-wallet JSON") ? 0 : 1; | ||
| 2903 | |||
| 2904 | string strPortableWallet = tmp + "/portable-wallet.dat"; | ||
| 2905 | string strBackupOut = tmp + "/backupwallet.txt"; | ||
| 2906 | vector<string> vBackupArgs; | ||
| 2907 | vBackupArgs.push_back("-datadir=" + strOriginal); | ||
| 2908 | vBackupArgs.push_back("-nomanagedtor"); | ||
| 2909 | vBackupArgs.push_back("-nogui"); | ||
| 2910 | vBackupArgs.push_back("-backupwallet=" + strPortableWallet); | ||
| 2911 | int nBackupRet = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut); | ||
| 2912 | nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()), | ||
| 2913 | "/backupwallet writes a portable wallet.dat") ? 0 : 1; | ||
| 2914 | |||
| 2915 | string strBackupOut2 = tmp + "/backupwallet-overwrite.txt"; | ||
| 2916 | int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2); | ||
| 2917 | nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) && | ||
| 2918 | !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") && | ||
| 2919 | !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."), | ||
| 2920 | "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1; | ||
| 2921 | |||
| 2922 | nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat", | ||
| 2923 | strRawCopy + "/wallet.dat"), | ||
| 2924 | "a raw wallet.dat can be copied without database logs") ? 0 : 1; | ||
| 2925 | string strRawAudit = tmp + "/raw-copy-audit.txt"; | ||
| 2926 | vector<string> vRawAuditArgs; | ||
| 2927 | vRawAuditArgs.push_back("-datadir=" + strRawCopy); | ||
| 2928 | vRawAuditArgs.push_back("-nomanagedtor"); | ||
| 2929 | vRawAuditArgs.push_back("-nogui"); | ||
| 2930 | vRawAuditArgs.push_back("-recoveryaudit"); | ||
| 2931 | int nRawAuditRet = RunBitflashChild(strExe, vRawAuditArgs, NULL, &strRawAudit); | ||
| 2932 | nFail += Check(nRawAuditRet == 0 && | ||
| 2933 | FileContainsText(strRawAudit, "recovery phrase: present") && | ||
| 2934 | !FileContainsText(strRawAudit, "Cannot open wallet.dat"), | ||
| 2935 | "a raw wallet.dat copy opens in a new datadir") ? 0 : 1; | ||
| 2936 | |||
| 2937 | nFail += Check(CopyFileLocal(strPortableWallet, | ||
| 2938 | strBackupCopy + "/wallet.dat"), | ||
| 2939 | "the portable backup can be installed as wallet.dat") ? 0 : 1; | ||
| 2940 | string strBackupAudit = tmp + "/backup-copy-audit.txt"; | ||
| 2941 | vector<string> vBackupAuditArgs; | ||
| 2942 | vBackupAuditArgs.push_back("-datadir=" + strBackupCopy); | ||
| 2943 | vBackupAuditArgs.push_back("-nomanagedtor"); | ||
| 2944 | vBackupAuditArgs.push_back("-nogui"); | ||
| 2945 | vBackupAuditArgs.push_back("-recoveryaudit"); | ||
| 2946 | int nBackupAuditRet = RunBitflashChild(strExe, vBackupAuditArgs, NULL, &strBackupAudit); | ||
| 2947 | nFail += Check(nBackupAuditRet == 0 && | ||
| 2948 | FileContainsText(strBackupAudit, "recovery phrase: present") && | ||
| 2949 | FileContainsText(strBackupAudit, "SLIP-0044 BITFLASH"), | ||
| 2950 | "a /backupwallet copy opens in a new datadir") ? 0 : 1; | ||
| 2951 | |||
| 2952 | string strPassFile = tmp + "/encrypt-pass.txt"; | ||
| 2953 | string strEncryptOut = tmp + "/encryptwallet.txt"; | ||
| 2954 | nFail += Check(WriteTextFile(strPassFile, "portable-test-passphrase\n"), | ||
| 2955 | "an encryption passphrase file can be written") ? 0 : 1; | ||
| 2956 | vector<string> vEncryptArgs; | ||
| 2957 | vEncryptArgs.push_back("-datadir=" + strOriginal); | ||
| 2958 | vEncryptArgs.push_back("-nomanagedtor"); | ||
| 2959 | vEncryptArgs.push_back("-nogui"); | ||
| 2960 | vEncryptArgs.push_back("-encryptwallet=@" + strPassFile); | ||
| 2961 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2962 | nFail += Check(nEncryptRet == 0 && | ||
| 2963 | !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."), | ||
| 2964 | "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1; | ||
| 2965 | |||
| 2966 | string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt"; | ||
| 2967 | vector<string> vEncryptedStorageAuditArgs; | ||
| 2968 | vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2969 | vEncryptedStorageAuditArgs.push_back("-nomanagedtor"); | ||
| 2970 | vEncryptedStorageAuditArgs.push_back("-nogui"); | ||
| 2971 | vEncryptedStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2972 | int nEncryptedStorageAuditRet = | ||
| 2973 | RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit); | ||
| 2974 | nFail += Check(nEncryptedStorageAuditRet == 0 && | ||
| 2975 | FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") && | ||
| 2976 | FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") && | ||
| 2977 | FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") && | ||
| 2978 | FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") && | ||
| 2979 | FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") && | ||
| 2980 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), | ||
| 2981 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; | ||
| 2982 | |||
| 2983 | string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; | ||
| 2984 | vector<string> vEncryptedStorageCheckArgs; | ||
| 2985 | vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 2986 | vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 2987 | vEncryptedStorageCheckArgs.push_back("-nogui"); | ||
| 2988 | vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 2989 | int nEncryptedStorageCheckRet = | ||
| 2990 | RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); | ||
| 2991 | nFail += Check(nEncryptedStorageCheckRet == 0 && | ||
| 2992 | FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && | ||
| 2993 | FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && | ||
| 2994 | FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), | ||
| 2995 | "the storage sanity check accepts an encrypted wallet") ? 0 : 1; | ||
| 2996 | |||
| 2997 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; | ||
| 2998 | vector<string> vEncryptedStorageAuditJsonArgs; | ||
| 2999 | vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | ||
| 3000 | vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor"); | ||
| 3001 | vEncryptedStorageAuditJsonArgs.push_back("-nogui"); | ||
| 3002 | vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + | ||
| 3003 | strEncryptedStorageAuditJson); | ||
| 3004 | int nEncryptedStorageAuditJsonRet = | ||
| 3005 | RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs); | ||
| 3006 | nFail += Check(nEncryptedStorageAuditJsonRet == 0 && | ||
| 3007 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3008 | "\"plain_private_keys\": 0") && | ||
| 3009 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3010 | "\"plain_hd_seed\": \"none\"") && | ||
| 3011 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3012 | "\"encrypted_hd_seed\": \"complete\"") && | ||
| 3013 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3014 | "\"wallet_minimum_version\": \"present\""), | ||
| 3015 | "the storage audit can write encrypted-wallet JSON") ? 0 : 1; | ||
| 3016 | } | ||
| 3017 | catch (const std::exception& e) | ||
| 3018 | { | ||
| 3019 | printf(" FAIL exception: %s\n", e.what()); | ||
| 3020 | nFail++; | ||
| 3021 | } | ||
| 3022 | catch (...) | ||
| 3023 | { | ||
| 3024 | printf(" FAIL unknown exception\n"); | ||
| 3025 | nFail++; | ||
| 3026 | } | ||
| 3027 | |||
| 3028 | RemoveTree(tmp); | ||
| 3029 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3030 | nFail, nFail == 1 ? "" : "s"); | ||
| 3031 | fflush(stdout); | ||
| 3032 | return nFail == 0 ? 0 : 1; | ||
| 3033 | } | ||
| 3034 | |||
| 1100 | static int RunNetMessageSelfTest() | 3035 | static int RunNetMessageSelfTest() |
| 1101 | { | 3036 | { |
| 1102 | fflush(stdout); | 3037 | fflush(stdout); |
| @@ -1105,6 +3040,13 @@ static int RunNetMessageSelfTest() | |||
| 1105 | int nFail = 0; | 3040 | int nFail = 0; |
| 1106 | try | 3041 | try |
| 1107 | { | 3042 | { |
| 3043 | // A peer that has not yet been kept waiting must not look like one that | ||
| 3044 | // has been waiting since the epoch. This is the whole bug: the stall | ||
| 3045 | // clock lives in CNode, so only the constructor can start it at zero. | ||
| 3046 | CNode opened(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 3047 | nFail += Check(opened.nMsgWaitSince == 0 && opened.nMsgWaitSize == 0, | ||
| 3048 | "a new peer starts with its stall clock stopped") ? 0 : 1; | ||
| 3049 | |||
| 1108 | CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); | 3050 | CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); |
| 1109 | complete.nVersion = VERSION; | 3051 | complete.nVersion = VERSION; |
| 1110 | complete.vRecv << CMessageHeader("ping", 0); | 3052 | complete.vRecv << CMessageHeader("ping", 0); |
| @@ -1119,20 +3061,38 @@ static int RunNetMessageSelfTest() | |||
| 1119 | nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; | 3061 | nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; |
| 1120 | nFail += Check(partial.vRecv.size() == nPartialBefore, | 3062 | nFail += Check(partial.vRecv.size() == nPartialBefore, |
| 1121 | "an incomplete message keeps one header in the buffer") ? 0 : 1; | 3063 | "an incomplete message keeps one header in the buffer") ? 0 : 1; |
| 1122 | nFail += Check(partial.nIncompleteMessageStart != 0, | 3064 | nFail += Check(partial.nMsgWaitSince != 0, |
| 1123 | "an incomplete message starts a timeout clock") ? 0 : 1; | 3065 | "an incomplete message starts the stall clock") ? 0 : 1; |
| 3066 | nFail += Check(partial.nMsgWaitSize == 0, | ||
| 3067 | "the stall clock records how much payload had arrived") ? 0 : 1; | ||
| 1124 | nFail += Check(!partial.fDisconnect, | 3068 | nFail += Check(!partial.fDisconnect, |
| 1125 | "a fresh incomplete message does not disconnect immediately") ? 0 : 1; | 3069 | "a fresh incomplete message does not disconnect immediately") ? 0 : 1; |
| 1126 | 3070 | ||
| 3071 | // Still arriving, just slowly: more payload is here than when the clock | ||
| 3072 | // was started, so the clock restarts however old it was. Timing the | ||
| 3073 | // wait instead of the stall would drop this peer. | ||
| 3074 | CNode slow(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 3075 | slow.nVersion = VERSION; | ||
| 3076 | slow.vRecv << CMessageHeader("block", 100); | ||
| 3077 | slow.vRecv.insert(slow.vRecv.end(), 50, '\0'); | ||
| 3078 | slow.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1; | ||
| 3079 | slow.nMsgWaitSize = 10; | ||
| 3080 | ProcessMessages(&slow); | ||
| 3081 | nFail += Check(!slow.fDisconnect, | ||
| 3082 | "a peer still sending payload is not dropped") ? 0 : 1; | ||
| 3083 | nFail += Check(slow.nMsgWaitSize == 50, | ||
| 3084 | "progress restarts the stall clock") ? 0 : 1; | ||
| 3085 | |||
| 3086 | // Nothing more has arrived since the clock started, and the clock is | ||
| 3087 | // older than the limit. | ||
| 1127 | CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); | 3088 | CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); |
| 1128 | stale.nVersion = VERSION; | 3089 | stale.nVersion = VERSION; |
| 1129 | stale.vRecv << CMessageHeader("block", 100); | 3090 | stale.vRecv << CMessageHeader("block", 100); |
| 1130 | stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; | 3091 | stale.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1; |
| 1131 | stale.nIncompleteMessageSize = 100; | 3092 | stale.nMsgWaitSize = 0; |
| 1132 | stale.strIncompleteMessageCommand = "block"; | ||
| 1133 | ProcessMessages(&stale); | 3093 | ProcessMessages(&stale); |
| 1134 | nFail += Check(stale.fDisconnect, | 3094 | nFail += Check(stale.fDisconnect, |
| 1135 | "a stale incomplete message disconnects the peer") ? 0 : 1; | 3095 | "a stalled incomplete message disconnects the peer") ? 0 : 1; |
| 1136 | 3096 | ||
| 1137 | CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); | 3097 | CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); |
| 1138 | oversized.nVersion = VERSION; | 3098 | oversized.nVersion = VERSION; |
| @@ -1158,6 +3118,87 @@ static int RunNetMessageSelfTest() | |||
| 1158 | return nFail == 0 ? 0 : 1; | 3118 | return nFail == 0 ? 0 : 1; |
| 1159 | } | 3119 | } |
| 1160 | 3120 | ||
| 3121 | static int RunNetworkParamsSelfTest() | ||
| 3122 | { | ||
| 3123 | fflush(stdout); | ||
| 3124 | printf("network-params self-test\n"); | ||
| 3125 | |||
| 3126 | int nFail = 0; | ||
| 3127 | std::string strOldDataDir = strSetDataDir; | ||
| 3128 | try | ||
| 3129 | { | ||
| 3130 | SelectChainParams(false); | ||
| 3131 | nFail += Check(!IsTestNet(), "starts on mainnet after explicit selection") ? 0 : 1; | ||
| 3132 | nFail += Check(ntohs(GetDefaultPort()) == MAINNET_PORT, | ||
| 3133 | "mainnet default P2P port is 8433") ? 0 : 1; | ||
| 3134 | CAddress mainAddr("127.0.0.1"); | ||
| 3135 | nFail += Check(mainAddr.port == htons(MAINNET_PORT), | ||
| 3136 | "mainnet address parsing uses the mainnet default port") ? 0 : 1; | ||
| 3137 | CMessageHeader mainHeader("ping", 0); | ||
| 3138 | char mainMagic[4]; | ||
| 3139 | memcpy(mainMagic, mainHeader.pchMessageStart, sizeof(mainMagic)); | ||
| 3140 | uint256 mainGenesis = hashGenesisBlock; | ||
| 3141 | uint256 mainMerkle = hashGenesisMerkleRoot; | ||
| 3142 | unsigned int mainNonce = GENESIS_NONCE; | ||
| 3143 | |||
| 3144 | SelectChainParams(true); | ||
| 3145 | nFail += Check(IsTestNet(), "testnet selection is sticky") ? 0 : 1; | ||
| 3146 | nFail += Check(ntohs(GetDefaultPort()) == TESTNET_PORT, | ||
| 3147 | "testnet default P2P port is 18433") ? 0 : 1; | ||
| 3148 | CAddress testAddr("127.0.0.1"); | ||
| 3149 | nFail += Check(testAddr.port == htons(TESTNET_PORT), | ||
| 3150 | "testnet address parsing uses the testnet default port") ? 0 : 1; | ||
| 3151 | CMessageHeader testHeader("ping", 0); | ||
| 3152 | nFail += Check(memcmp(mainMagic, testHeader.pchMessageStart, sizeof(mainMagic)) != 0, | ||
| 3153 | "testnet message magic differs from mainnet") ? 0 : 1; | ||
| 3154 | nFail += Check(hashGenesisBlock != mainGenesis && | ||
| 3155 | hashGenesisMerkleRoot != mainMerkle && | ||
| 3156 | GENESIS_NONCE != mainNonce, | ||
| 3157 | "testnet genesis constants differ from mainnet") ? 0 : 1; | ||
| 3158 | nFail += Check(hashGenesisBlock == uint256("0xa9d11c6d697bcb7aea0653bc088b12dcf9f44e732df205162cf085fd9d9963eb") && | ||
| 3159 | hashGenesisMerkleRoot == uint256("0x510b293105e3ff85b8d4c3920e357f85d1a86bf199eb4e7d11f04f380030ec5c") && | ||
| 3160 | GENESIS_NONCE == 3199, | ||
| 3161 | "testnet genesis constants are pinned") ? 0 : 1; | ||
| 3162 | |||
| 3163 | std::string tmp; | ||
| 3164 | if (!MakeTempDir(tmp)) | ||
| 3165 | { | ||
| 3166 | nFail += Check(false, "creates a temporary data directory") ? 0 : 1; | ||
| 3167 | } | ||
| 3168 | else | ||
| 3169 | { | ||
| 3170 | strSetDataDir = tmp; | ||
| 3171 | SelectChainParams(false); | ||
| 3172 | std::string mainDir = GetAppDir(); | ||
| 3173 | SelectChainParams(true); | ||
| 3174 | std::string testDir = GetAppDir(); | ||
| 3175 | nFail += Check(mainDir == tmp, | ||
| 3176 | "mainnet uses the selected data directory") ? 0 : 1; | ||
| 3177 | nFail += Check(testDir == tmp + "/testnet", | ||
| 3178 | "testnet stores data below a testnet subdirectory") ? 0 : 1; | ||
| 3179 | RemoveTree(tmp); | ||
| 3180 | } | ||
| 3181 | } | ||
| 3182 | catch (const std::exception& e) | ||
| 3183 | { | ||
| 3184 | printf(" FAIL exception: %s\n", e.what()); | ||
| 3185 | nFail++; | ||
| 3186 | } | ||
| 3187 | catch (...) | ||
| 3188 | { | ||
| 3189 | printf(" FAIL unknown exception\n"); | ||
| 3190 | nFail++; | ||
| 3191 | } | ||
| 3192 | |||
| 3193 | strSetDataDir = strOldDataDir; | ||
| 3194 | SelectChainParams(false); | ||
| 3195 | |||
| 3196 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3197 | nFail, nFail == 1 ? "" : "s"); | ||
| 3198 | fflush(stdout); | ||
| 3199 | return nFail == 0 ? 0 : 1; | ||
| 3200 | } | ||
| 3201 | |||
| 1161 | static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) | 3202 | static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) |
| 1162 | { | 3203 | { |
| 1163 | CTransaction tx; | 3204 | CTransaction tx; |
| @@ -1175,6 +3216,74 @@ static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) | |||
| 1175 | return block; | 3216 | return block; |
| 1176 | } | 3217 | } |
| 1177 | 3218 | ||
| 3219 | // A distinct transaction of a chosen weight and verification cost. nUnique | ||
| 3220 | // only keeps the txids apart, so a mempool built from these is not a pile of | ||
| 3221 | // duplicates a block could never carry anyway. | ||
| 3222 | static CTransaction MakeSizedTx(size_t nPadBytes, unsigned int nSigOps, int nUnique) | ||
| 3223 | { | ||
| 3224 | CTransaction tx; | ||
| 3225 | tx.vin.push_back(CTxIn(COutPoint(), CScript() << (int64)nUnique << 1)); | ||
| 3226 | tx.vout.push_back(CTxOut(0, CScript())); | ||
| 3227 | for (unsigned int i = 0; i < nSigOps; i++) | ||
| 3228 | tx.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 3229 | tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nPadBytes, 0); | ||
| 3230 | return tx; | ||
| 3231 | } | ||
| 3232 | |||
| 3233 | static CBlock MakeCoinbaseOnlyTemplate() | ||
| 3234 | { | ||
| 3235 | CTransaction txNew; | ||
| 3236 | txNew.vin.resize(1); | ||
| 3237 | txNew.vin[0].prevout.SetNull(); | ||
| 3238 | txNew.vin[0].scriptSig << 1 << 1 << 1; | ||
| 3239 | txNew.vout.resize(1); | ||
| 3240 | txNew.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 3241 | |||
| 3242 | CBlock block; | ||
| 3243 | block.vtx.push_back(txNew); | ||
| 3244 | block.nVersion = 1; | ||
| 3245 | block.nTime = GetAdjustedTime(); | ||
| 3246 | block.nBits = bnProofOfWorkLimit.GetCompact(); | ||
| 3247 | block.nNonce = 0; | ||
| 3248 | return block; | ||
| 3249 | } | ||
| 3250 | |||
| 3251 | // The sum CheckBlock() takes, spelled the same way. | ||
| 3252 | static unsigned int BlockSigOpCount(const CBlock& block) | ||
| 3253 | { | ||
| 3254 | unsigned int n = 0; | ||
| 3255 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 3256 | n += block.vtx[i].GetSigOpCount(); | ||
| 3257 | return n; | ||
| 3258 | } | ||
| 3259 | |||
| 3260 | // Drive the selection rule the way both assemblers drive it: ask | ||
| 3261 | // BlockTemplateFits before appending, skip what does not fit, never stop early | ||
| 3262 | // on one that does not. | ||
| 3263 | // | ||
| 3264 | // This mirrors the loops in BitcoinMiner() and RebuildJob() rather than calling | ||
| 3265 | // them -- one needs a CTxDB, a wallet key and cs_main, the other is static in | ||
| 3266 | // rpc.cpp, so neither is reachable from here. What that leaves proven is the | ||
| 3267 | // rule the two of them share and the property it is meant to guarantee, which | ||
| 3268 | // is where the defect actually lived: the budget was right and the enforcement | ||
| 3269 | // was not. | ||
| 3270 | static void SelectIntoTemplate(CBlock& block, const vector<CTransaction>& vMempool) | ||
| 3271 | { | ||
| 3272 | unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK); | ||
| 3273 | unsigned int nBlockSigOps = block.vtx[0].GetSigOpCount(); | ||
| 3274 | for (int i = 0; i < (int)vMempool.size(); i++) | ||
| 3275 | { | ||
| 3276 | const CTransaction& tx = vMempool[i]; | ||
| 3277 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 3278 | unsigned int nTxSigOps = tx.GetSigOpCount(); | ||
| 3279 | if (!BlockTemplateFits(nBlockSize, nBlockSigOps, nTxSize, nTxSigOps)) | ||
| 3280 | continue; | ||
| 3281 | block.vtx.push_back(tx); | ||
| 3282 | nBlockSize += nTxSize; | ||
| 3283 | nBlockSigOps += nTxSigOps; | ||
| 3284 | } | ||
| 3285 | } | ||
| 3286 | |||
| 1178 | static int RunConsensusLimitsSelfTest() | 3287 | static int RunConsensusLimitsSelfTest() |
| 1179 | { | 3288 | { |
| 1180 | fflush(stdout); | 3289 | fflush(stdout); |
| @@ -1198,6 +3307,95 @@ static int RunConsensusLimitsSelfTest() | |||
| 1198 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; | 3307 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; |
| 1199 | nFail += Check(!oversized.CheckSizeLimits(), | 3308 | nFail += Check(!oversized.CheckSizeLimits(), |
| 1200 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; | 3309 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; |
| 3310 | |||
| 3311 | // The other half of the cap. Everything above asks what validation | ||
| 3312 | // refuses; nothing asked what assembly produces, which is why a miner | ||
| 3313 | // that could overshoot the cap by an entire mempool went unnoticed. | ||
| 3314 | nFail += Check(MAX_BLOCK_SIZE_GEN < MAX_BLOCK_SIZE, | ||
| 3315 | "the assembly budget stays under the consensus cap") ? 0 : 1; | ||
| 3316 | |||
| 3317 | // The rule itself, at its edges. The sigop comparison has to match | ||
| 3318 | // CheckBlock(), which fails on `> MAX_BLOCK_SIGOPS` and so accepts a | ||
| 3319 | // block sitting exactly on the limit. | ||
| 3320 | nFail += Check(BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 100, 0), | ||
| 3321 | "a transaction that exactly fills the size budget fits") ? 0 : 1; | ||
| 3322 | nFail += Check(!BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 101, 0), | ||
| 3323 | "one byte past the size budget does not fit") ? 0 : 1; | ||
| 3324 | nFail += Check(BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS), | ||
| 3325 | "sigops exactly on the limit fit, as CheckBlock() allows") ? 0 : 1; | ||
| 3326 | nFail += Check(!BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS + 1), | ||
| 3327 | "one sigop past the limit does not fit") ? 0 : 1; | ||
| 3328 | |||
| 3329 | // A mempool several times the consensus cap, in ordinary-sized | ||
| 3330 | // transactions. The old miner tested its budget only on the outer | ||
| 3331 | // loop, so a single pass appended all of this. | ||
| 3332 | { | ||
| 3333 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3334 | vector<CTransaction> vMempool; | ||
| 3335 | for (int i = 0; i < 300; i++) | ||
| 3336 | vMempool.push_back(MakeSizedTx(10000, 1, i)); | ||
| 3337 | SelectIntoTemplate(block, vMempool); | ||
| 3338 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3339 | |||
| 3340 | nFail += Check(block.vtx.size() > 1, | ||
| 3341 | "a full mempool still fills a template") ? 0 : 1; | ||
| 3342 | nFail += Check(::GetSerializeSize(block, SER_DISK) <= MAX_BLOCK_SIZE_GEN, | ||
| 3343 | "a template built from a 3 MB mempool respects the budget") ? 0 : 1; | ||
| 3344 | nFail += Check(block.CheckSizeLimits(), | ||
| 3345 | "that template is one CheckBlock() would accept on size") ? 0 : 1; | ||
| 3346 | } | ||
| 3347 | |||
| 3348 | // One transaction too big for the budget. Nothing on the path into the | ||
| 3349 | // mempool caps transaction size, so this is a transaction a peer can | ||
| 3350 | // simply send. It must be skipped and the rest still taken -- skipped, | ||
| 3351 | // not stopped on. | ||
| 3352 | { | ||
| 3353 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3354 | vector<CTransaction> vMempool; | ||
| 3355 | vMempool.push_back(MakeSizedTx(1500000, 1, 9999)); | ||
| 3356 | for (int i = 0; i < 10; i++) | ||
| 3357 | vMempool.push_back(MakeSizedTx(1000, 1, i)); | ||
| 3358 | |||
| 3359 | unsigned int nHugeSize = ::GetSerializeSize(vMempool[0], SER_NETWORK); | ||
| 3360 | nFail += Check(nHugeSize > MAX_BLOCK_SIZE && nHugeSize <= MAX_SIZE, | ||
| 3361 | "the outsized transaction is one the mempool would hold") ? 0 : 1; | ||
| 3362 | |||
| 3363 | SelectIntoTemplate(block, vMempool); | ||
| 3364 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3365 | |||
| 3366 | bool fTookHuge = false; | ||
| 3367 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 3368 | if (block.vtx[i].GetHash() == vMempool[0].GetHash()) | ||
| 3369 | fTookHuge = true; | ||
| 3370 | |||
| 3371 | nFail += Check(!fTookHuge, | ||
| 3372 | "a transaction larger than the budget is left out") ? 0 : 1; | ||
| 3373 | nFail += Check(block.vtx.size() == 11, | ||
| 3374 | "the ten that do fit are taken anyway") ? 0 : 1; | ||
| 3375 | nFail += Check(block.CheckSizeLimits(), | ||
| 3376 | "one outsized transaction cannot invalidate a template") ? 0 : 1; | ||
| 3377 | } | ||
| 3378 | |||
| 3379 | // Transactions that are cheap in bytes and expensive to verify. Size | ||
| 3380 | // alone does not bound what a block costs, and CheckBlock() refuses on | ||
| 3381 | // the count regardless of how small the block is. | ||
| 3382 | { | ||
| 3383 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3384 | vector<CTransaction> vMempool; | ||
| 3385 | for (int i = 0; i < 100; i++) | ||
| 3386 | vMempool.push_back(MakeSizedTx(0, 1000, i)); | ||
| 3387 | SelectIntoTemplate(block, vMempool); | ||
| 3388 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3389 | |||
| 3390 | nFail += Check(block.vtx.size() > 1, | ||
| 3391 | "sigop-heavy transactions are still selected") ? 0 : 1; | ||
| 3392 | nFail += Check(BlockSigOpCount(block) <= MAX_BLOCK_SIGOPS, | ||
| 3393 | "selection stops at the sigop cap CheckBlock() enforces") ? 0 : 1; | ||
| 3394 | nFail += Check(::GetSerializeSize(block, SER_DISK) < MAX_BLOCK_SIZE_GEN, | ||
| 3395 | "and stops on sigops while still well inside the size budget") ? 0 : 1; | ||
| 3396 | nFail += Check(block.CheckSizeLimits(), | ||
| 3397 | "the sigop-bounded template is within the size limit too") ? 0 : 1; | ||
| 3398 | } | ||
| 1201 | } | 3399 | } |
| 1202 | catch (const std::exception& e) | 3400 | catch (const std::exception& e) |
| 1203 | { | 3401 | { |
| @@ -1216,6 +3414,357 @@ static int RunConsensusLimitsSelfTest() | |||
| 1216 | return nFail == 0 ? 0 : 1; | 3414 | return nFail == 0 ? 0 : 1; |
| 1217 | } | 3415 | } |
| 1218 | 3416 | ||
| 3417 | static int RunParseMoneySelfTest() | ||
| 3418 | { | ||
| 3419 | printf("parse-money self-test\n"); | ||
| 3420 | int nFail = 0; | ||
| 3421 | struct Case { const char* in; bool ok; int64 val; }; | ||
| 3422 | Case cases[] = { | ||
| 3423 | {"0.5", true, 50000000LL}, | ||
| 3424 | {"1", true, 100000000LL}, | ||
| 3425 | {"1.23456789", true, 123456789LL}, | ||
| 3426 | {"0.00000001", true, 1LL}, | ||
| 3427 | {"10.00000000", true, 1000000000LL}, | ||
| 3428 | {"1,000", true, 100000000000LL}, | ||
| 3429 | {"1.5 ", true, 150000000LL}, | ||
| 3430 | {"0.123456789", false, 0LL}, | ||
| 3431 | {"1.2.3", false, 0LL}, | ||
| 3432 | {"abc", false, 0LL}, | ||
| 3433 | {"-1", false, 0LL}, | ||
| 3434 | {"1e5", false, 0LL}, | ||
| 3435 | }; | ||
| 3436 | for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) | ||
| 3437 | { | ||
| 3438 | int64 v = -1; | ||
| 3439 | bool r = ParseMoney(cases[i].in, v); | ||
| 3440 | bool pass = (r == cases[i].ok) && (!r || v == cases[i].val); | ||
| 3441 | char desc[160]; | ||
| 3442 | snprintf(desc, sizeof(desc), "ParseMoney(\"%s\") %s and value matches", | ||
| 3443 | cases[i].in, cases[i].ok ? "accepts" : "rejects"); | ||
| 3444 | nFail += Check(pass, desc) ? 0 : 1; | ||
| 3445 | } | ||
| 3446 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3447 | nFail, nFail == 1 ? "" : "s"); | ||
| 3448 | fflush(stdout); | ||
| 3449 | return nFail == 0 ? 0 : 1; | ||
| 3450 | } | ||
| 3451 | |||
| 3452 | static bool SelfTestReadN(SOCKET s, void* buf, int n) | ||
| 3453 | { | ||
| 3454 | char* p = (char*)buf; | ||
| 3455 | int off = 0; | ||
| 3456 | while (off < n) | ||
| 3457 | { | ||
| 3458 | int r = recv(s, p + off, n - off, 0); | ||
| 3459 | if (r <= 0) | ||
| 3460 | return false; | ||
| 3461 | off += r; | ||
| 3462 | } | ||
| 3463 | return true; | ||
| 3464 | } | ||
| 3465 | |||
| 3466 | static bool SelfTestWriteN(SOCKET s, const void* buf, int n) | ||
| 3467 | { | ||
| 3468 | const char* p = (const char*)buf; | ||
| 3469 | int off = 0; | ||
| 3470 | while (off < n) | ||
| 3471 | { | ||
| 3472 | int r = send(s, p + off, n - off, 0); | ||
| 3473 | if (r <= 0) | ||
| 3474 | return false; | ||
| 3475 | off += r; | ||
| 3476 | } | ||
| 3477 | return true; | ||
| 3478 | } | ||
| 3479 | |||
| 3480 | static void SelfTestSetSocketTimeout(SOCKET s, int nTimeoutSecs) | ||
| 3481 | { | ||
| 3482 | #ifdef _WIN32 | ||
| 3483 | DWORD tv = (DWORD)nTimeoutSecs * 1000; | ||
| 3484 | setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); | ||
| 3485 | setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)); | ||
| 3486 | #else | ||
| 3487 | struct timeval tv; | ||
| 3488 | tv.tv_sec = nTimeoutSecs; | ||
| 3489 | tv.tv_usec = 0; | ||
| 3490 | setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); | ||
| 3491 | setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); | ||
| 3492 | #endif | ||
| 3493 | } | ||
| 3494 | |||
| 3495 | #ifdef _WIN32 | ||
| 3496 | struct SelfTestWinsock | ||
| 3497 | { | ||
| 3498 | bool fStarted; | ||
| 3499 | SelfTestWinsock() : fStarted(false) | ||
| 3500 | { | ||
| 3501 | WSADATA wsadata; | ||
| 3502 | fStarted = (WSAStartup(MAKEWORD(2,2), &wsadata) == 0); | ||
| 3503 | } | ||
| 3504 | ~SelfTestWinsock() | ||
| 3505 | { | ||
| 3506 | if (fStarted) | ||
| 3507 | WSACleanup(); | ||
| 3508 | } | ||
| 3509 | }; | ||
| 3510 | #endif | ||
| 3511 | |||
| 3512 | static bool RunSocks5HandshakeProbe(std::string& errOut) | ||
| 3513 | { | ||
| 3514 | errOut.clear(); | ||
| 3515 | #ifdef _WIN32 | ||
| 3516 | SelfTestWinsock winsock; | ||
| 3517 | if (!winsock.fStarted) | ||
| 3518 | { | ||
| 3519 | errOut = "could not start Winsock"; | ||
| 3520 | return false; | ||
| 3521 | } | ||
| 3522 | #endif | ||
| 3523 | |||
| 3524 | SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); | ||
| 3525 | if (listener == INVALID_SOCKET) | ||
| 3526 | { | ||
| 3527 | errOut = "could not create listener"; | ||
| 3528 | return false; | ||
| 3529 | } | ||
| 3530 | SelfTestSetSocketTimeout(listener, 5); | ||
| 3531 | |||
| 3532 | struct sockaddr_in addr; | ||
| 3533 | memset(&addr, 0, sizeof(addr)); | ||
| 3534 | addr.sin_family = AF_INET; | ||
| 3535 | addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); | ||
| 3536 | addr.sin_port = 0; | ||
| 3537 | if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 || | ||
| 3538 | listen(listener, 1) != 0) | ||
| 3539 | { | ||
| 3540 | closesocket(listener); | ||
| 3541 | errOut = "could not bind/listen on loopback"; | ||
| 3542 | return false; | ||
| 3543 | } | ||
| 3544 | |||
| 3545 | #ifdef _WIN32 | ||
| 3546 | int addrLen = sizeof(addr); | ||
| 3547 | #else | ||
| 3548 | socklen_t addrLen = sizeof(addr); | ||
| 3549 | #endif | ||
| 3550 | if (getsockname(listener, (struct sockaddr*)&addr, &addrLen) != 0) | ||
| 3551 | { | ||
| 3552 | closesocket(listener); | ||
| 3553 | errOut = "could not read listener port"; | ||
| 3554 | return false; | ||
| 3555 | } | ||
| 3556 | unsigned short proxyPort = ntohs(addr.sin_port); | ||
| 3557 | |||
| 3558 | bool fServerOk = false; | ||
| 3559 | std::string serverErr; | ||
| 3560 | std::string requestedHost; | ||
| 3561 | unsigned short requestedPort = 0; | ||
| 3562 | std::thread server([&]() { | ||
| 3563 | SOCKET s = accept(listener, NULL, NULL); | ||
| 3564 | if (s == INVALID_SOCKET) | ||
| 3565 | { | ||
| 3566 | serverErr = "accept failed"; | ||
| 3567 | return; | ||
| 3568 | } | ||
| 3569 | SelfTestSetSocketTimeout(s, 5); | ||
| 3570 | |||
| 3571 | unsigned char hello[3] = {0,0,0}; | ||
| 3572 | if (!SelfTestReadN(s, hello, sizeof(hello)) || | ||
| 3573 | hello[0] != 0x05 || hello[1] != 0x01 || hello[2] != 0x00) | ||
| 3574 | { | ||
| 3575 | serverErr = "bad SOCKS5 greeting"; | ||
| 3576 | closesocket(s); | ||
| 3577 | return; | ||
| 3578 | } | ||
| 3579 | unsigned char choice[2] = {0x05, 0x00}; | ||
| 3580 | if (!SelfTestWriteN(s, choice, sizeof(choice))) | ||
| 3581 | { | ||
| 3582 | serverErr = "could not write method choice"; | ||
| 3583 | closesocket(s); | ||
| 3584 | return; | ||
| 3585 | } | ||
| 3586 | |||
| 3587 | unsigned char hdr[5] = {0,0,0,0,0}; | ||
| 3588 | if (!SelfTestReadN(s, hdr, sizeof(hdr)) || | ||
| 3589 | hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00 || hdr[3] != 0x03) | ||
| 3590 | { | ||
| 3591 | serverErr = "CONNECT did not use domain-name address type"; | ||
| 3592 | closesocket(s); | ||
| 3593 | return; | ||
| 3594 | } | ||
| 3595 | |||
| 3596 | unsigned char len = hdr[4]; | ||
| 3597 | std::vector<unsigned char> rest((size_t)len + 2); | ||
| 3598 | if (!SelfTestReadN(s, &rest[0], (int)rest.size())) | ||
| 3599 | { | ||
| 3600 | serverErr = "could not read CONNECT target"; | ||
| 3601 | closesocket(s); | ||
| 3602 | return; | ||
| 3603 | } | ||
| 3604 | requestedHost.assign((const char*)&rest[0], (size_t)len); | ||
| 3605 | requestedPort = ((unsigned short)rest[len] << 8) | rest[len + 1]; | ||
| 3606 | |||
| 3607 | unsigned char reply[10] = {0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0}; | ||
| 3608 | if (!SelfTestWriteN(s, reply, sizeof(reply))) | ||
| 3609 | { | ||
| 3610 | serverErr = "could not write CONNECT reply"; | ||
| 3611 | closesocket(s); | ||
| 3612 | return; | ||
| 3613 | } | ||
| 3614 | fServerOk = true; | ||
| 3615 | closesocket(s); | ||
| 3616 | }); | ||
| 3617 | |||
| 3618 | std::string err; | ||
| 3619 | BtfClearSocks5Proxy(); | ||
| 3620 | bool fClientOk = BtfSetSocks5Proxy(strprintf("127.0.0.1:%u", (unsigned)proxyPort), err); | ||
| 3621 | SOCKET client = INVALID_SOCKET; | ||
| 3622 | if (fClientOk) | ||
| 3623 | client = BtfConnectSocket("relay.example", 443, SOCK_NOSTR, 5); | ||
| 3624 | if (client != INVALID_SOCKET) | ||
| 3625 | BtfCloseSocket(client); | ||
| 3626 | BtfClearSocks5Proxy(); | ||
| 3627 | closesocket(listener); | ||
| 3628 | server.join(); | ||
| 3629 | |||
| 3630 | if (!fClientOk) | ||
| 3631 | errOut = strprintf("could not enable proxy: %s", err.c_str()); | ||
| 3632 | else if (client == INVALID_SOCKET) | ||
| 3633 | errOut = "client SOCKS5 connect failed"; | ||
| 3634 | else if (!fServerOk) | ||
| 3635 | errOut = serverErr.empty() ? "mock server failed" : serverErr; | ||
| 3636 | else if (requestedHost != "relay.example" || requestedPort != 443) | ||
| 3637 | errOut = strprintf("unexpected CONNECT target %s:%u", requestedHost.c_str(), (unsigned)requestedPort); | ||
| 3638 | else | ||
| 3639 | return true; | ||
| 3640 | return false; | ||
| 3641 | } | ||
| 3642 | |||
| 3643 | static int RunSocks5ProxySelfTest() | ||
| 3644 | { | ||
| 3645 | printf("socks5-proxy self-test\n"); | ||
| 3646 | int nFail = 0; | ||
| 3647 | std::string host, err; | ||
| 3648 | unsigned short port = 0; | ||
| 3649 | |||
| 3650 | nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) && | ||
| 3651 | host == "127.0.0.1" && port == 9050, | ||
| 3652 | "parses local Tor proxy endpoint") ? 0 : 1; | ||
| 3653 | nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) && | ||
| 3654 | host == "localhost" && port == 9050, | ||
| 3655 | "parses hostname proxy endpoint") ? 0 : 1; | ||
| 3656 | nFail += Check(BtfParseSocks5Proxy("[::1]:9050", host, port, err) && | ||
| 3657 | host == "::1" && port == 9050, | ||
| 3658 | "parses bracketed IPv6 proxy endpoint") ? 0 : 1; | ||
| 3659 | nFail += Check(!BtfParseSocks5Proxy("::1:9050", host, port, err), | ||
| 3660 | "rejects unbracketed IPv6 proxy endpoint") ? 0 : 1; | ||
| 3661 | nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err), | ||
| 3662 | "rejects missing port") ? 0 : 1; | ||
| 3663 | nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err), | ||
| 3664 | "rejects zero port") ? 0 : 1; | ||
| 3665 | nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err), | ||
| 3666 | "rejects out-of-range port") ? 0 : 1; | ||
| 3667 | nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err), | ||
| 3668 | "rejects overflowing port") ? 0 : 1; | ||
| 3669 | nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err), | ||
| 3670 | "rejects unsupported authenticated proxy syntax") ? 0 : 1; | ||
| 3671 | nFail += Check(BtfIsTorOnionHost("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion") && | ||
| 3672 | BtfIsTorOnionHost("Relay.Example.ONION.") && | ||
| 3673 | !BtfIsTorOnionHost("relay.example") && | ||
| 3674 | !BtfIsTorOnionHost(".onion"), | ||
| 3675 | "detects .onion hosts without accepting lookalikes") ? 0 : 1; | ||
| 3676 | std::string onion; | ||
| 3677 | nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) && | ||
| 3678 | onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433", | ||
| 3679 | "normalizes v3 onion peer endpoint") ? 0 : 1; | ||
| 3680 | nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) && | ||
| 3681 | !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) && | ||
| 3682 | !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion), | ||
| 3683 | "rejects malformed onion peer endpoints") ? 0 : 1; | ||
| 3684 | |||
| 3685 | BtfClearSocks5Proxy(); | ||
| 3686 | nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; | ||
| 3687 | nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) && | ||
| 3688 | BtfSocks5ProxyEnabled() && | ||
| 3689 | BtfSocks5ProxyName() == "127.0.0.1:9050", | ||
| 3690 | "enables parsed SOCKS5 proxy") ? 0 : 1; | ||
| 3691 | BtfClearSocks5Proxy(); | ||
| 3692 | nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1; | ||
| 3693 | |||
| 3694 | nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) && | ||
| 3695 | BtfSocks5ProxyEnabled() && | ||
| 3696 | BtfSocks5ProxyName() == "[::1]:9050", | ||
| 3697 | "enables bracketed IPv6 proxy endpoint") ? 0 : 1; | ||
| 3698 | BtfClearSocks5Proxy(); | ||
| 3699 | |||
| 3700 | nFail += Check(BtfEnableTorProxy("", err) && | ||
| 3701 | BtfSocks5ProxyEnabled() && | ||
| 3702 | BtfTorProxyEnabled() && | ||
| 3703 | BtfSocks5ProxyName() == "127.0.0.1:9050", | ||
| 3704 | "enables Tor mode on the default local SOCKS5 endpoint") ? 0 : 1; | ||
| 3705 | BtfClearSocks5Proxy(); | ||
| 3706 | |||
| 3707 | nFail += Check(BtfEnableTorProxy("[::1]:9050", err) && | ||
| 3708 | BtfSocks5ProxyEnabled() && | ||
| 3709 | BtfTorProxyEnabled() && | ||
| 3710 | BtfSocks5ProxyName() == "[::1]:9050", | ||
| 3711 | "enables Tor mode on a custom IPv6 endpoint") ? 0 : 1; | ||
| 3712 | BtfClearSocks5Proxy(); | ||
| 3713 | |||
| 3714 | nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9051", err) && | ||
| 3715 | BtfSocks5ProxyEnabled() && | ||
| 3716 | !BtfTorProxyEnabled(), | ||
| 3717 | "plain SOCKS5 mode is distinct from Tor mode") ? 0 : 1; | ||
| 3718 | BtfClearSocks5Proxy(); | ||
| 3719 | |||
| 3720 | std::string handshakeErr; | ||
| 3721 | bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr); | ||
| 3722 | nFail += Check(fHandshakeOk, handshakeErr.empty() ? | ||
| 3723 | "completes SOCKS5 domain-name CONNECT handshake" : | ||
| 3724 | strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)", | ||
| 3725 | handshakeErr.c_str()).c_str()) ? 0 : 1; | ||
| 3726 | |||
| 3727 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3728 | nFail, nFail == 1 ? "" : "s"); | ||
| 3729 | fflush(stdout); | ||
| 3730 | return nFail == 0 ? 0 : 1; | ||
| 3731 | } | ||
| 3732 | |||
| 3733 | static int RunManagedTorSelfTest() | ||
| 3734 | { | ||
| 3735 | printf("managed-tor self-test\n"); | ||
| 3736 | int nFail = 0; | ||
| 3737 | |||
| 3738 | std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data", | ||
| 3739 | "C:/Bitflash Managed Tor/onion-service", | ||
| 3740 | 19050, 19051, 8433); | ||
| 3741 | nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos, | ||
| 3742 | "quotes the Tor data directory") ? 0 : 1; | ||
| 3743 | nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos, | ||
| 3744 | "binds SOCKS5 to localhost only") ? 0 : 1; | ||
| 3745 | nFail += Check(torrc.find("ControlPort 127.0.0.1:19051") != std::string::npos && | ||
| 3746 | torrc.find("CookieAuthentication 1") != std::string::npos, | ||
| 3747 | "binds an authenticated Tor control port to localhost only") ? 0 : 1; | ||
| 3748 | nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos && | ||
| 3749 | torrc.find("IsolateClientAddr") != std::string::npos && | ||
| 3750 | torrc.find("IsolateDestAddr") != std::string::npos && | ||
| 3751 | torrc.find("IsolateDestPort") != std::string::npos, | ||
| 3752 | "enables strict Tor stream isolation") ? 0 : 1; | ||
| 3753 | nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos, | ||
| 3754 | "writes a hidden service directory") ? 0 : 1; | ||
| 3755 | nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos, | ||
| 3756 | "requests a v3 onion service") ? 0 : 1; | ||
| 3757 | nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos, | ||
| 3758 | "maps the onion service to the Bitflash P2P listener") ? 0 : 1; | ||
| 3759 | nFail += Check(BtfManagedTorStatus() == "disabled", | ||
| 3760 | "managed Tor starts disabled") ? 0 : 1; | ||
| 3761 | |||
| 3762 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3763 | nFail, nFail == 1 ? "" : "s"); | ||
| 3764 | fflush(stdout); | ||
| 3765 | return nFail == 0 ? 0 : 1; | ||
| 3766 | } | ||
| 3767 | |||
| 1219 | int RunSelfTest(const std::string& name) | 3768 | int RunSelfTest(const std::string& name) |
| 1220 | { | 3769 | { |
| 1221 | AttachTerminal(); | 3770 | AttachTerminal(); |
| @@ -1226,16 +3775,44 @@ int RunSelfTest(const std::string& name) | |||
| 1226 | return RunWalletHDSelfTest(); | 3775 | return RunWalletHDSelfTest(); |
| 1227 | if (name == "wallet-format") | 3776 | if (name == "wallet-format") |
| 1228 | return RunWalletFormatSelfTest(); | 3777 | return RunWalletFormatSelfTest(); |
| 3778 | if (name == "wallet-storage-sanity") | ||
| 3779 | return RunWalletStorageSanitySelfTest(); | ||
| 3780 | if (name == "db-env-reopen") | ||
| 3781 | return RunDbEnvReopenSelfTest(); | ||
| 3782 | if (name == "wallet-sqlite") | ||
| 3783 | return RunWalletSQLiteSelfTest(); | ||
| 3784 | if (name == "wallet-sqlite-migration") | ||
| 3785 | return RunWalletSQLiteMigrationSelfTest(); | ||
| 1229 | if (name == "wallet-crypto") | 3786 | if (name == "wallet-crypto") |
| 1230 | return RunWalletCryptoSelfTest(); | 3787 | return RunWalletCryptoSelfTest(); |
| 1231 | if (name == "wallet-encrypt") | 3788 | if (name == "wallet-encrypt") |
| 1232 | return RunWalletEncryptSelfTest(); | 3789 | return RunWalletEncryptSelfTest(); |
| 3790 | if (name == "wallet-sqlite-encrypt") | ||
| 3791 | return RunWalletSQLiteEncryptSelfTest(); | ||
| 3792 | if (name == "wallet-backend-default") | ||
| 3793 | return RunWalletBackendDefaultSelfTest(); | ||
| 3794 | if (name == "wallet-convert") | ||
| 3795 | return RunWalletConvertSelfTest(); | ||
| 3796 | if (name == "wallet-portability") | ||
| 3797 | return RunWalletPortabilitySelfTest(); | ||
| 1233 | if (name == "net-message") | 3798 | if (name == "net-message") |
| 1234 | return RunNetMessageSelfTest(); | 3799 | return RunNetMessageSelfTest(); |
| 3800 | if (name == "network-params") | ||
| 3801 | return RunNetworkParamsSelfTest(); | ||
| 1235 | if (name == "consensus-limits") | 3802 | if (name == "consensus-limits") |
| 1236 | return RunConsensusLimitsSelfTest(); | 3803 | return RunConsensusLimitsSelfTest(); |
| 3804 | if (name == "pool-stratum") | ||
| 3805 | return RunPoolStratumSelfTest(); | ||
| 3806 | if (name == "payout-ledger") | ||
| 3807 | return RunPayoutLedgerSelfTest(); | ||
| 3808 | if (name == "parse-money") | ||
| 3809 | return RunParseMoneySelfTest(); | ||
| 3810 | if (name == "socks5-proxy") | ||
| 3811 | return RunSocks5ProxySelfTest(); | ||
| 3812 | if (name == "managed-tor") | ||
| 3813 | return RunManagedTorSelfTest(); | ||
| 1237 | 3814 | ||
| 1238 | printf("Unknown self-test '%s'\n", name.c_str()); | 3815 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 1239 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); | 3816 | 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-backend-default, wallet-convert, wallet-portability, net-message, network-params, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n"); |
| 1240 | return 1; | 3817 | return 1; |
| 1241 | } | 3818 | } |