diff options
Diffstat (limited to 'src/selftest.cpp')
| -rw-r--r-- | src/selftest.cpp | 1241 |
1 files changed, 1241 insertions, 0 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp new file mode 100644 index 0000000..ff0289b --- /dev/null +++ b/src/selftest.cpp | |||
| @@ -0,0 +1,1241 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // Integration self-tests that need the real wallet/database code linked into | ||
| 5 | // the node. These are intentionally run behind an explicit command-line flag | ||
| 6 | // and against a temporary data directory. | ||
| 7 | |||
| 8 | #include "headers_core.h" | ||
| 9 | #include "bip32.h" | ||
| 10 | #include "selftest.h" | ||
| 11 | #include "walletcmd.h" | ||
| 12 | |||
| 13 | // Test results go to the terminal, not to debug.log. | ||
| 14 | // | ||
| 15 | // printf in this tree is OutputDebugStringF, which writes into the data | ||
| 16 | // directory's debug.log and binds that path, once, on its first call. For a | ||
| 17 | // self-test that produced two bad outcomes at the same time. On Windows the | ||
| 18 | // binary is linked -mwindows and has no console, so `make tests` printed the | ||
| 19 | // bip32 results, echoed the self-test command, and then showed nothing at all -- | ||
| 20 | // a failure was still caught, because a non-zero return stops make, but it | ||
| 21 | // arrived with no way to tell which check failed. And because the first line | ||
| 22 | // printed before strSetDataDir was pointed at the temporary directory, every | ||
| 23 | // line landed in the developer's real debug.log: the same file users are asked | ||
| 24 | // to paste into issues. | ||
| 25 | // | ||
| 26 | // Undefining the macro here gives this file the real printf, so results reach | ||
| 27 | // stdout. Nothing else in the tree is affected. | ||
| 28 | #undef printf | ||
| 29 | |||
| 30 | #include <mutex> | ||
| 31 | #include <stdexcept> | ||
| 32 | #include <thread> | ||
| 33 | |||
| 34 | #ifdef _WIN32 | ||
| 35 | #include <direct.h> | ||
| 36 | #include <fcntl.h> | ||
| 37 | #include <io.h> | ||
| 38 | #else | ||
| 39 | #include <dirent.h> | ||
| 40 | #include <sys/stat.h> | ||
| 41 | #include <sys/types.h> | ||
| 42 | #include <unistd.h> | ||
| 43 | #endif | ||
| 44 | |||
| 45 | static std::string HexStrLocal(const std::vector<unsigned char>& v) | ||
| 46 | { | ||
| 47 | static const char* hex = "0123456789abcdef"; | ||
| 48 | std::string out; | ||
| 49 | out.reserve(v.size() * 2); | ||
| 50 | for (unsigned char c : v) | ||
| 51 | { | ||
| 52 | out.push_back(hex[c >> 4]); | ||
| 53 | out.push_back(hex[c & 0x0f]); | ||
| 54 | } | ||
| 55 | return out; | ||
| 56 | } | ||
| 57 | |||
| 58 | static bool MakeTempDir(std::string& out) | ||
| 59 | { | ||
| 60 | #ifdef _WIN32 | ||
| 61 | char base[MAX_PATH + 1] = {0}; | ||
| 62 | if (!GetTempPathA(MAX_PATH, base)) | ||
| 63 | return false; | ||
| 64 | for (int i = 0; i < 100; i++) | ||
| 65 | { | ||
| 66 | std::string path = strprintf("%sbitflash-selftest-%lu-%lld-%d", | ||
| 67 | base, (unsigned long)GetCurrentProcessId(), (long long)GetTime(), i); | ||
| 68 | if (CreateDirectoryA(path.c_str(), NULL)) | ||
| 69 | { | ||
| 70 | out = path; | ||
| 71 | return true; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return false; | ||
| 75 | #else | ||
| 76 | char tmpl[256]; | ||
| 77 | snprintf(tmpl, sizeof(tmpl), "/tmp/bitflash-selftest-%ld-XXXXXX", (long)getpid()); | ||
| 78 | char* p = mkdtemp(tmpl); | ||
| 79 | if (!p) | ||
| 80 | return false; | ||
| 81 | out = p; | ||
| 82 | return true; | ||
| 83 | #endif | ||
| 84 | } | ||
| 85 | |||
| 86 | static bool GetCurrentDir(std::string& out) | ||
| 87 | { | ||
| 88 | #ifdef _WIN32 | ||
| 89 | char buf[MAX_PATH + 1] = {0}; | ||
| 90 | DWORD n = GetCurrentDirectoryA(MAX_PATH, buf); | ||
| 91 | if (n == 0 || n > MAX_PATH) | ||
| 92 | return false; | ||
| 93 | out = buf; | ||
| 94 | return true; | ||
| 95 | #else | ||
| 96 | char buf[4096]; | ||
| 97 | if (!getcwd(buf, sizeof(buf))) | ||
| 98 | return false; | ||
| 99 | out = buf; | ||
| 100 | return true; | ||
| 101 | #endif | ||
| 102 | } | ||
| 103 | |||
| 104 | static bool SetCurrentDir(const std::string& path) | ||
| 105 | { | ||
| 106 | #ifdef _WIN32 | ||
| 107 | return SetCurrentDirectoryA(path.c_str()) != 0; | ||
| 108 | #else | ||
| 109 | return chdir(path.c_str()) == 0; | ||
| 110 | #endif | ||
| 111 | } | ||
| 112 | |||
| 113 | static void RemoveTree(const std::string& path) | ||
| 114 | { | ||
| 115 | #ifdef _WIN32 | ||
| 116 | DWORD attr = GetFileAttributesA(path.c_str()); | ||
| 117 | if (attr == INVALID_FILE_ATTRIBUTES) | ||
| 118 | return; | ||
| 119 | if (!(attr & FILE_ATTRIBUTE_DIRECTORY)) | ||
| 120 | { | ||
| 121 | DeleteFileA(path.c_str()); | ||
| 122 | return; | ||
| 123 | } | ||
| 124 | |||
| 125 | WIN32_FIND_DATAA findData; | ||
| 126 | std::string pattern = path + "\\*"; | ||
| 127 | HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); | ||
| 128 | if (hFind != INVALID_HANDLE_VALUE) | ||
| 129 | { | ||
| 130 | do | ||
| 131 | { | ||
| 132 | std::string name = findData.cFileName; | ||
| 133 | if (name == "." || name == "..") | ||
| 134 | continue; | ||
| 135 | RemoveTree(path + "\\" + name); | ||
| 136 | } | ||
| 137 | while (FindNextFileA(hFind, &findData)); | ||
| 138 | FindClose(hFind); | ||
| 139 | } | ||
| 140 | RemoveDirectoryA(path.c_str()); | ||
| 141 | #else | ||
| 142 | struct stat st; | ||
| 143 | if (lstat(path.c_str(), &st) != 0) | ||
| 144 | return; | ||
| 145 | if (!S_ISDIR(st.st_mode)) | ||
| 146 | { | ||
| 147 | unlink(path.c_str()); | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | |||
| 151 | DIR* dir = opendir(path.c_str()); | ||
| 152 | if (dir) | ||
| 153 | { | ||
| 154 | struct dirent* ent; | ||
| 155 | while ((ent = readdir(dir)) != NULL) | ||
| 156 | { | ||
| 157 | std::string name = ent->d_name; | ||
| 158 | if (name == "." || name == "..") | ||
| 159 | continue; | ||
| 160 | RemoveTree(path + "/" + name); | ||
| 161 | } | ||
| 162 | closedir(dir); | ||
| 163 | } | ||
| 164 | rmdir(path.c_str()); | ||
| 165 | #endif | ||
| 166 | } | ||
| 167 | |||
| 168 | static bool WalletHasPrivateKey(const std::vector<unsigned char>& vchPubKey) | ||
| 169 | { | ||
| 170 | CPrivKey priv; | ||
| 171 | return CWalletDB("r").ReadKey(vchPubKey, priv) && !priv.empty(); | ||
| 172 | } | ||
| 173 | |||
| 174 | static bool Check(bool condition, const char* message) | ||
| 175 | { | ||
| 176 | if (condition) | ||
| 177 | { | ||
| 178 | printf(" ok %s\n", message); | ||
| 179 | return true; | ||
| 180 | } | ||
| 181 | printf(" FAIL %s\n", message); | ||
| 182 | return false; | ||
| 183 | } | ||
| 184 | |||
| 185 | static int RunWalletKeyPoolSelfTest() | ||
| 186 | { | ||
| 187 | fflush(stdout); | ||
| 188 | printf("wallet-keypool self-test\n"); | ||
| 189 | |||
| 190 | std::string tmp; | ||
| 191 | std::string cwd; | ||
| 192 | if (!MakeTempDir(tmp)) | ||
| 193 | { | ||
| 194 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 195 | return 1; | ||
| 196 | } | ||
| 197 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 198 | { | ||
| 199 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 200 | RemoveTree(tmp); | ||
| 201 | return 1; | ||
| 202 | } | ||
| 203 | |||
| 204 | int nFail = 0; | ||
| 205 | // Before anything can open the database or resolve the log path: what the | ||
| 206 | // node itself prints during the test belongs in the temporary directory, | ||
| 207 | // and dies with it. The test's own results are on stdout, above. | ||
| 208 | strSetDataDir = tmp; | ||
| 209 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 210 | |||
| 211 | try | ||
| 212 | { | ||
| 213 | if (!LoadWallet()) | ||
| 214 | throw std::runtime_error("LoadWallet failed"); | ||
| 215 | |||
| 216 | TopUpKeyPool(); | ||
| 217 | int nInitialPoolSize = 0; | ||
| 218 | CRITICAL_BLOCK(cs_keyPool) | ||
| 219 | nInitialPoolSize = (int)mapKeyPool.size(); | ||
| 220 | nFail += Check(nInitialPoolSize == KEYPOOL_SIZE, "TopUpKeyPool fills the pool") ? 0 : 1; | ||
| 221 | |||
| 222 | bool fPooledKeysStored = true; | ||
| 223 | CRITICAL_BLOCK(cs_keyPool) | ||
| 224 | { | ||
| 225 | for (map<int64, vector<unsigned char> >::const_iterator mi = mapKeyPool.begin(); | ||
| 226 | mi != mapKeyPool.end(); ++mi) | ||
| 227 | if (!WalletHasPrivateKey(mi->second)) | ||
| 228 | fPooledKeysStored = false; | ||
| 229 | } | ||
| 230 | nFail += Check(fPooledKeysStored, "every pooled public key has a stored private key") ? 0 : 1; | ||
| 231 | |||
| 232 | std::vector<std::vector<unsigned char> > drawn; | ||
| 233 | std::mutex drawnMutex; | ||
| 234 | const int nThreads = 8; | ||
| 235 | const int nDrawsPerThread = 32; | ||
| 236 | std::vector<std::thread> threads; | ||
| 237 | |||
| 238 | for (int t = 0; t < nThreads; t++) | ||
| 239 | { | ||
| 240 | threads.emplace_back([&drawn, &drawnMutex, nDrawsPerThread]() { | ||
| 241 | for (int i = 0; i < nDrawsPerThread; i++) | ||
| 242 | { | ||
| 243 | std::vector<unsigned char> key = GetKeyFromPool(); | ||
| 244 | std::lock_guard<std::mutex> lock(drawnMutex); | ||
| 245 | drawn.push_back(key); | ||
| 246 | } | ||
| 247 | }); | ||
| 248 | } | ||
| 249 | for (std::thread& t : threads) | ||
| 250 | t.join(); | ||
| 251 | |||
| 252 | nFail += Check((int)drawn.size() == nThreads * nDrawsPerThread, | ||
| 253 | "all concurrent draws returned a key") ? 0 : 1; | ||
| 254 | |||
| 255 | std::set<std::string> seen; | ||
| 256 | bool fUnique = true; | ||
| 257 | bool fAllStored = true; | ||
| 258 | for (const std::vector<unsigned char>& key : drawn) | ||
| 259 | { | ||
| 260 | if (!seen.insert(HexStrLocal(key)).second) | ||
| 261 | fUnique = false; | ||
| 262 | if (!WalletHasPrivateKey(key)) | ||
| 263 | fAllStored = false; | ||
| 264 | } | ||
| 265 | |||
| 266 | nFail += Check(fUnique, "concurrent draws never return the same key") ? 0 : 1; | ||
| 267 | nFail += Check(fAllStored, "each drawn key was already stored in wallet.dat") ? 0 : 1; | ||
| 268 | int nPoolSize = 0; | ||
| 269 | CRITICAL_BLOCK(cs_keyPool) | ||
| 270 | nPoolSize = (int)mapKeyPool.size(); | ||
| 271 | nFail += Check(nPoolSize == KEYPOOL_SIZE, "drawing leaves the key pool topped up") ? 0 : 1; | ||
| 272 | } | ||
| 273 | catch (const std::exception& e) | ||
| 274 | { | ||
| 275 | printf(" FAIL exception: %s\n", e.what()); | ||
| 276 | nFail++; | ||
| 277 | } | ||
| 278 | catch (...) | ||
| 279 | { | ||
| 280 | printf(" FAIL unknown exception\n"); | ||
| 281 | nFail++; | ||
| 282 | } | ||
| 283 | |||
| 284 | DBFlush(true); | ||
| 285 | SetCurrentDir(cwd); | ||
| 286 | RemoveTree(tmp); | ||
| 287 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 288 | nFail, nFail == 1 ? "" : "s"); | ||
| 289 | fflush(stdout); | ||
| 290 | return nFail == 0 ? 0 : 1; | ||
| 291 | } | ||
| 292 | |||
| 293 | // AttachTerminal() moved to util.h: the wallet phrase commands need it too, | ||
| 294 | // and two copies of a console-attach that must get its one condition right is | ||
| 295 | // one copy too many. | ||
| 296 | |||
| 297 | static int RunWalletHDSelfTest() | ||
| 298 | { | ||
| 299 | fflush(stdout); | ||
| 300 | printf("wallet-hd self-test\n"); | ||
| 301 | |||
| 302 | std::string tmp; | ||
| 303 | std::string cwd; | ||
| 304 | if (!MakeTempDir(tmp)) | ||
| 305 | { | ||
| 306 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 307 | return 1; | ||
| 308 | } | ||
| 309 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 310 | { | ||
| 311 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 312 | RemoveTree(tmp); | ||
| 313 | return 1; | ||
| 314 | } | ||
| 315 | |||
| 316 | int nFail = 0; | ||
| 317 | strSetDataDir = tmp; | ||
| 318 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 319 | |||
| 320 | // Test-vector phrases, so nothing here depends on randomness. | ||
| 321 | const std::string strPhraseA = | ||
| 322 | "abandon abandon abandon abandon abandon abandon " | ||
| 323 | "abandon abandon abandon abandon abandon about"; | ||
| 324 | const std::string strPhraseB = | ||
| 325 | "legal winner thank year wave sausage worth useful " | ||
| 326 | "legal winner thank yellow"; | ||
| 327 | |||
| 328 | try | ||
| 329 | { | ||
| 330 | if (!LoadWallet()) | ||
| 331 | throw std::runtime_error("LoadWallet failed"); | ||
| 332 | |||
| 333 | std::string strError; | ||
| 334 | TopUpKeyPool(); | ||
| 335 | int nRandomPoolSize = 0; | ||
| 336 | CRITICAL_BLOCK(cs_keyPool) | ||
| 337 | nRandomPoolSize = (int)mapKeyPool.size(); | ||
| 338 | nFail += Check(nRandomPoolSize == KEYPOOL_SIZE, | ||
| 339 | "a wallet can start with a random key pool") ? 0 : 1; | ||
| 340 | |||
| 341 | // The address the wallet was showing before any of this. It has to | ||
| 342 | // survive under a name that says it is not covered by the phrase. | ||
| 343 | std::vector<unsigned char> vchPreSeedKey = keyUser.GetPubKey(); | ||
| 344 | std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); | ||
| 345 | |||
| 346 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), | ||
| 347 | "a valid phrase installs a seed") ? 0 : 1; | ||
| 348 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; | ||
| 349 | nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, | ||
| 350 | "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; | ||
| 351 | nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 352 | "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; | ||
| 353 | nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, | ||
| 354 | "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; | ||
| 355 | std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 356 | HD_BIP44_ACCOUNT, | ||
| 357 | HD_BIP44_CHAIN_RECEIVE, | ||
| 358 | 0); | ||
| 359 | nFail += Check(vBIP44Path.size() == 5 && | ||
| 360 | vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && | ||
| 361 | vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && | ||
| 362 | vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && | ||
| 363 | vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && | ||
| 364 | vBIP44Path[4] == 0, | ||
| 365 | "the BIP44 helper builds m/44'/coin_type'/account'/change/index") ? 0 : 1; | ||
| 366 | |||
| 367 | int nPoolAfterSeed = 0; | ||
| 368 | CRITICAL_BLOCK(cs_keyPool) | ||
| 369 | nPoolAfterSeed = (int)mapKeyPool.size(); | ||
| 370 | nFail += Check(nPoolAfterSeed == 0, | ||
| 371 | "installing a phrase clears the old random key pool") ? 0 : 1; | ||
| 372 | nFail += Check(nHDNext == 0, | ||
| 373 | "installing a BIP44 phrase leaves the legacy counter unused") ? 0 : 1; | ||
| 374 | |||
| 375 | CKey keyFirstDerived; | ||
| 376 | if (!DeriveHDKey(0, keyFirstDerived, strError)) | ||
| 377 | throw std::runtime_error("default derivation failed: " + strError); | ||
| 378 | bitflash::BIP32PrivateNode hdParent; | ||
| 379 | hdParent.privateKey = vchHDMaster; | ||
| 380 | hdParent.chainCode = vchHDChainCode; | ||
| 381 | bitflash::BIP32PrivateNode hdBIP44ReceiveChild; | ||
| 382 | if (!bitflash::BIP32DerivePath(hdParent, | ||
| 383 | HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 384 | HD_BIP44_ACCOUNT, | ||
| 385 | HD_BIP44_CHAIN_RECEIVE, | ||
| 386 | 0), | ||
| 387 | hdBIP44ReceiveChild, | ||
| 388 | strError)) | ||
| 389 | throw std::runtime_error("BIP44 receive path derivation failed: " + strError); | ||
| 390 | CKey keyBIP44ReceivePath; | ||
| 391 | if (!keyBIP44ReceivePath.SetSecret(hdBIP44ReceiveChild.privateKey)) | ||
| 392 | throw std::runtime_error("BIP44 receive path produced an unusable key"); | ||
| 393 | nFail += Check(keyBIP44ReceivePath.GetPubKey() == keyFirstDerived.GetPubKey(), | ||
| 394 | "the default HD key path is m/44'/coin_type'/0'/0/0") ? 0 : 1; | ||
| 395 | |||
| 396 | bitflash::BIP32PrivateNode hdLegacyChild; | ||
| 397 | int nSchemaForLegacyCheck = nHDKeySchema; | ||
| 398 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 399 | CKey keyLegacyDerived; | ||
| 400 | if (!DeriveHDKey(0, keyLegacyDerived, strError)) | ||
| 401 | throw std::runtime_error("legacy derivation failed: " + strError); | ||
| 402 | nHDKeySchema = nSchemaForLegacyCheck; | ||
| 403 | if (!bitflash::BIP32DerivePath(hdParent, HDLegacyPath(0), hdLegacyChild, strError)) | ||
| 404 | throw std::runtime_error("legacy path derivation failed: " + strError); | ||
| 405 | CKey keyLegacyPath; | ||
| 406 | if (!keyLegacyPath.SetSecret(hdLegacyChild.privateKey)) | ||
| 407 | throw std::runtime_error("legacy path produced an unusable key"); | ||
| 408 | nFail += Check(keyLegacyPath.GetPubKey() == keyLegacyDerived.GetPubKey(), | ||
| 409 | "the legacy HD key path remains m/index'") ? 0 : 1; | ||
| 410 | |||
| 411 | std::vector<unsigned char> vchDefaultKey; | ||
| 412 | bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey); | ||
| 413 | nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(), | ||
| 414 | "the default receiving key is derived from the phrase") ? 0 : 1; | ||
| 415 | nFail += Check(WalletHasPrivateKey(vchDefaultKey), | ||
| 416 | "the derived default key is stored in wallet.dat") ? 0 : 1; | ||
| 417 | |||
| 418 | std::string strNewAddr = PubKeyToAddress(vchDefaultKey); | ||
| 419 | nFail += Check(strPreSeedAddr != strNewAddr, | ||
| 420 | "the visible address stops being the pre-seed one") ? 0 : 1; | ||
| 421 | nFail += Check(mapAddressBook.count(strPreSeedAddr) > 0 && | ||
| 422 | mapAddressBook[strPreSeedAddr] != | ||
| 423 | mapAddressBook[strNewAddr], | ||
| 424 | "the pre-seed address is kept, named apart from the new one") ? 0 : 1; | ||
| 425 | |||
| 426 | std::vector<unsigned char> vchBefore = vchHDMaster; | ||
| 427 | int nSchemaBefore = nHDKeySchema; | ||
| 428 | unsigned int nCoinTypeBefore = nHDCoinType; | ||
| 429 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), | ||
| 430 | "an invalid phrase is refused") ? 0 : 1; | ||
| 431 | nFail += Check(vchHDMaster == vchBefore, | ||
| 432 | "a refused phrase leaves the existing seed alone") ? 0 : 1; | ||
| 433 | nFail += Check(nHDKeySchema == nSchemaBefore, | ||
| 434 | "a refused phrase leaves the derivation schema alone") ? 0 : 1; | ||
| 435 | nFail += Check(nHDCoinType == nCoinTypeBefore, | ||
| 436 | "a refused phrase leaves the BIP44 coin type alone") ? 0 : 1; | ||
| 437 | |||
| 438 | // The property the whole feature exists for: the same words give back | ||
| 439 | // the same keys, in the same order. | ||
| 440 | std::vector<std::string> first; | ||
| 441 | for (unsigned int i = 0; i < 5; i++) | ||
| 442 | { | ||
| 443 | CKey key; | ||
| 444 | if (!DeriveHDKey(i, key, strError)) | ||
| 445 | throw std::runtime_error("derivation failed: " + strError); | ||
| 446 | first.push_back(HexStrLocal(key.GetPubKey())); | ||
| 447 | } | ||
| 448 | |||
| 449 | SetHDSeedFromMnemonic(strPhraseA, strError); | ||
| 450 | std::vector<std::string> again; | ||
| 451 | for (unsigned int i = 0; i < 5; i++) | ||
| 452 | { | ||
| 453 | CKey key; | ||
| 454 | if (!DeriveHDKey(i, key, strError)) | ||
| 455 | throw std::runtime_error("derivation failed: " + strError); | ||
| 456 | again.push_back(HexStrLocal(key.GetPubKey())); | ||
| 457 | } | ||
| 458 | nFail += Check(first == again, | ||
| 459 | "the same phrase derives the same keys in the same order") ? 0 : 1; | ||
| 460 | |||
| 461 | SetHDSeedFromMnemonic(strPhraseB, strError); | ||
| 462 | std::vector<std::string> other; | ||
| 463 | for (unsigned int i = 0; i < 5; i++) | ||
| 464 | { | ||
| 465 | CKey key; | ||
| 466 | if (!DeriveHDKey(i, key, strError)) | ||
| 467 | throw std::runtime_error("derivation failed: " + strError); | ||
| 468 | other.push_back(HexStrLocal(key.GetPubKey())); | ||
| 469 | } | ||
| 470 | nFail += Check(first != other, "a different phrase derives different keys") ? 0 : 1; | ||
| 471 | |||
| 472 | std::set<std::string> distinct(first.begin(), first.end()); | ||
| 473 | nFail += Check(distinct.size() == first.size(), | ||
| 474 | "consecutive indices give distinct keys") ? 0 : 1; | ||
| 475 | |||
| 476 | // With a seed installed the pool must be derived from it, and the | ||
| 477 | // counter must move exactly once per key. | ||
| 478 | SetHDSeedFromMnemonic(strPhraseA, strError); | ||
| 479 | unsigned int nReceiveNextBefore = nHDReceiveNext; | ||
| 480 | TopUpKeyPool(); | ||
| 481 | |||
| 482 | int nPool = 0; | ||
| 483 | CRITICAL_BLOCK(cs_keyPool) | ||
| 484 | nPool = (int)mapKeyPool.size(); | ||
| 485 | nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; | ||
| 486 | nFail += Check(nHDReceiveNext == nReceiveNextBefore + (unsigned int)KEYPOOL_SIZE, | ||
| 487 | "the BIP44 receive counter advances once per pooled key") ? 0 : 1; | ||
| 488 | nFail += Check(nHDChangeNext == 0, | ||
| 489 | "filling the receive pool leaves the BIP44 change counter alone") ? 0 : 1; | ||
| 490 | nFail += Check(!RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 400, 600, 600), | ||
| 491 | "BIP44 restore depth is not satisfied by receive plus legacy alone") ? 0 : 1; | ||
| 492 | nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 600, 600, 600), | ||
| 493 | "BIP44 restore depth is satisfied on each branch") ? 0 : 1; | ||
| 494 | nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_LEGACY, 0, 0, 600, 600), | ||
| 495 | "legacy restore depth still follows the legacy counter") ? 0 : 1; | ||
| 496 | |||
| 497 | std::set<std::string> derived; | ||
| 498 | for (unsigned int i = nReceiveNextBefore; i < nHDReceiveNext; i++) | ||
| 499 | { | ||
| 500 | CKey key; | ||
| 501 | if (!DeriveHDKey(i, key, strError)) | ||
| 502 | throw std::runtime_error("derivation failed: " + strError); | ||
| 503 | derived.insert(HexStrLocal(key.GetPubKey())); | ||
| 504 | } | ||
| 505 | |||
| 506 | bool fStored = true; | ||
| 507 | bool fFromSeed = true; | ||
| 508 | CRITICAL_BLOCK(cs_keyPool) | ||
| 509 | { | ||
| 510 | for (map<int64, vector<unsigned char> >::const_iterator mi = mapKeyPool.begin(); | ||
| 511 | mi != mapKeyPool.end(); ++mi) | ||
| 512 | { | ||
| 513 | if (!WalletHasPrivateKey(mi->second)) | ||
| 514 | fStored = false; | ||
| 515 | if (!derived.count(HexStrLocal(mi->second))) | ||
| 516 | fFromSeed = false; | ||
| 517 | } | ||
| 518 | } | ||
| 519 | nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; | ||
| 520 | nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; | ||
| 521 | |||
| 522 | // A recovery phrase is only useful if the wallet can tell the user | ||
| 523 | // what today's spendable balance would actually come back from it. | ||
| 524 | CKey keyAuditDerived; | ||
| 525 | if (!DeriveHDKey(0, keyAuditDerived, strError)) | ||
| 526 | throw std::runtime_error("audit derivation failed: " + strError); | ||
| 527 | CKey keyAuditChange; | ||
| 528 | if (!DeriveHDChangeKey(0, keyAuditChange, strError)) | ||
| 529 | throw std::runtime_error("audit change derivation failed: " + strError); | ||
| 530 | nFail += Check(keyAuditChange.GetPubKey() != keyAuditDerived.GetPubKey(), | ||
| 531 | "BIP44 receive and change chains derive different keys") ? 0 : 1; | ||
| 532 | if (!AddKey(keyAuditChange)) | ||
| 533 | throw std::runtime_error("could not store the audit change key"); | ||
| 534 | nHDChangeNext = 1; | ||
| 535 | int nSchemaForLegacyAudit = nHDKeySchema; | ||
| 536 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 537 | CKey keyAuditLegacyHD; | ||
| 538 | if (!DeriveHDKey(0, keyAuditLegacyHD, strError)) | ||
| 539 | throw std::runtime_error("audit legacy compatibility derivation failed: " + strError); | ||
| 540 | nHDKeySchema = nSchemaForLegacyAudit; | ||
| 541 | if (!AddKey(keyAuditLegacyHD)) | ||
| 542 | throw std::runtime_error("could not store the audit legacy compatibility key"); | ||
| 543 | nHDNext = 1; | ||
| 544 | |||
| 545 | CWalletTx wtxLegacy; | ||
| 546 | wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | ||
| 547 | CWalletTx wtxDerived; | ||
| 548 | wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); | ||
| 549 | CWalletTx wtxChange; | ||
| 550 | wtxChange.vout.push_back(CTxOut(13 * COIN, CScript() << keyAuditChange.GetPubKey() << OP_CHECKSIG)); | ||
| 551 | CWalletTx wtxLegacyHD; | ||
| 552 | wtxLegacyHD.vout.push_back(CTxOut(17 * COIN, CScript() << keyAuditLegacyHD.GetPubKey() << OP_CHECKSIG)); | ||
| 553 | CWalletTx wtxImmatureLegacy; | ||
| 554 | wtxImmatureLegacy.vin.push_back(CTxIn()); | ||
| 555 | wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | ||
| 556 | |||
| 557 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 558 | { | ||
| 559 | mapWallet.clear(); | ||
| 560 | mapWallet[wtxLegacy.GetHash()] = wtxLegacy; | ||
| 561 | mapWallet[wtxDerived.GetHash()] = wtxDerived; | ||
| 562 | mapWallet[wtxChange.GetHash()] = wtxChange; | ||
| 563 | mapWallet[wtxLegacyHD.GetHash()] = wtxLegacyHD; | ||
| 564 | mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; | ||
| 565 | } | ||
| 566 | |||
| 567 | WalletRecoveryAudit audit = GetWalletRecoveryAudit(); | ||
| 568 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; | ||
| 569 | nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, | ||
| 570 | "the recovery audit reports the derivation schema") ? 0 : 1; | ||
| 571 | nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 572 | "the recovery audit reports the BIP44 coin type") ? 0 : 1; | ||
| 573 | nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, | ||
| 574 | "the recovery audit reports receive/change counters") ? 0 : 1; | ||
| 575 | nFail += Check(audit.nLegacyCredit == 5 * COIN, | ||
| 576 | "the recovery audit finds wallet.dat-only balance") ? 0 : 1; | ||
| 577 | nFail += Check(audit.nRecoverableCredit == 37 * COIN, | ||
| 578 | "the recovery audit finds BIP44 and legacy-HD phrase balance") ? 0 : 1; | ||
| 579 | nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 3, | ||
| 580 | "the recovery audit counts wallet.dat-only and phrase-backed transactions") ? 0 : 1; | ||
| 581 | nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, | ||
| 582 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; | ||
| 583 | nFail += Check(audit.nLegacyImmatureTx == 1, | ||
| 584 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; | ||
| 585 | } | ||
| 586 | catch (const std::exception& e) | ||
| 587 | { | ||
| 588 | printf(" FAIL exception: %s\n", e.what()); | ||
| 589 | nFail++; | ||
| 590 | } | ||
| 591 | catch (...) | ||
| 592 | { | ||
| 593 | printf(" FAIL unknown exception\n"); | ||
| 594 | nFail++; | ||
| 595 | } | ||
| 596 | |||
| 597 | DBFlush(true); | ||
| 598 | SetCurrentDir(cwd); | ||
| 599 | RemoveTree(tmp); | ||
| 600 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 601 | nFail, nFail == 1 ? "" : "s"); | ||
| 602 | fflush(stdout); | ||
| 603 | return nFail == 0 ? 0 : 1; | ||
| 604 | } | ||
| 605 | |||
| 606 | static int RunWalletFormatSelfTest() | ||
| 607 | { | ||
| 608 | fflush(stdout); | ||
| 609 | printf("wallet-format self-test\n"); | ||
| 610 | |||
| 611 | std::string tmp; | ||
| 612 | std::string cwd; | ||
| 613 | if (!MakeTempDir(tmp)) | ||
| 614 | { | ||
| 615 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 616 | return 1; | ||
| 617 | } | ||
| 618 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 619 | { | ||
| 620 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 621 | RemoveTree(tmp); | ||
| 622 | return 1; | ||
| 623 | } | ||
| 624 | |||
| 625 | int nFail = 0; | ||
| 626 | strSetDataDir = tmp; | ||
| 627 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 628 | |||
| 629 | try | ||
| 630 | { | ||
| 631 | class CWalletDBRaw : public CWalletDB | ||
| 632 | { | ||
| 633 | public: | ||
| 634 | CWalletDBRaw(const char* pszMode="r+") : CWalletDB(pszMode) { } | ||
| 635 | bool WriteStringRecord(const string& strType, int nValue) | ||
| 636 | { | ||
| 637 | return Write(strType, nValue); | ||
| 638 | } | ||
| 639 | bool EraseStringRecord(const string& strType) | ||
| 640 | { | ||
| 641 | return Erase(strType); | ||
| 642 | } | ||
| 643 | }; | ||
| 644 | |||
| 645 | if (!LoadWallet()) | ||
| 646 | throw std::runtime_error("LoadWallet failed"); | ||
| 647 | |||
| 648 | { | ||
| 649 | CWalletDB walletdb; | ||
| 650 | nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED), | ||
| 651 | "the current wallet format marker can be written") ? 0 : 1; | ||
| 652 | } | ||
| 653 | |||
| 654 | std::vector<unsigned char> vchDefaultKey; | ||
| 655 | nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), | ||
| 656 | "the current wallet format marker loads") ? 0 : 1; | ||
| 657 | |||
| 658 | { | ||
| 659 | CWalletDBRaw walletdb; | ||
| 660 | nFail += Check(walletdb.WriteStringRecord("mkey", 1), | ||
| 661 | "a malformed encrypted master-key record can be written") ? 0 : 1; | ||
| 662 | } | ||
| 663 | |||
| 664 | vchDefaultKey.clear(); | ||
| 665 | nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), | ||
| 666 | "a malformed encrypted master-key record is refused") ? 0 : 1; | ||
| 667 | |||
| 668 | { | ||
| 669 | CWalletDBRaw walletdb; | ||
| 670 | nFail += Check(walletdb.EraseStringRecord("mkey"), | ||
| 671 | "the malformed encrypted master-key record can be removed") ? 0 : 1; | ||
| 672 | } | ||
| 673 | |||
| 674 | { | ||
| 675 | CWalletDB walletdb; | ||
| 676 | nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED + 1), | ||
| 677 | "a future wallet format marker can be written") ? 0 : 1; | ||
| 678 | } | ||
| 679 | |||
| 680 | vchDefaultKey.clear(); | ||
| 681 | nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), | ||
| 682 | "a future wallet format marker is refused") ? 0 : 1; | ||
| 683 | } | ||
| 684 | catch (const std::exception& e) | ||
| 685 | { | ||
| 686 | printf(" FAIL exception: %s\n", e.what()); | ||
| 687 | nFail++; | ||
| 688 | } | ||
| 689 | catch (...) | ||
| 690 | { | ||
| 691 | printf(" FAIL unknown exception\n"); | ||
| 692 | nFail++; | ||
| 693 | } | ||
| 694 | |||
| 695 | DBFlush(true); | ||
| 696 | SetCurrentDir(cwd); | ||
| 697 | RemoveTree(tmp); | ||
| 698 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 699 | nFail, nFail == 1 ? "" : "s"); | ||
| 700 | fflush(stdout); | ||
| 701 | return nFail == 0 ? 0 : 1; | ||
| 702 | } | ||
| 703 | |||
| 704 | static void ClearWalletRuntimeForTest() | ||
| 705 | { | ||
| 706 | CRITICAL_BLOCK(cs_mapKeys) | ||
| 707 | { | ||
| 708 | mapKeys.clear(); | ||
| 709 | mapCryptedKeys.clear(); | ||
| 710 | mapPubKeys.clear(); | ||
| 711 | mapMasterKeys.clear(); | ||
| 712 | vWalletMasterKey.clear(); | ||
| 713 | vchCryptedHDMaster.clear(); | ||
| 714 | vchCryptedHDChainCode.clear(); | ||
| 715 | fWalletEncrypted = false; | ||
| 716 | fWalletLocked = true; | ||
| 717 | } | ||
| 718 | CRITICAL_BLOCK(cs_keyPool) | ||
| 719 | mapKeyPool.clear(); | ||
| 720 | vchHDMaster.clear(); | ||
| 721 | vchHDChainCode.clear(); | ||
| 722 | nHDNext = 0; | ||
| 723 | } | ||
| 724 | |||
| 725 | static int RunWalletCryptoSelfTest() | ||
| 726 | { | ||
| 727 | fflush(stdout); | ||
| 728 | printf("wallet-crypto self-test\n"); | ||
| 729 | |||
| 730 | std::string tmp; | ||
| 731 | std::string cwd; | ||
| 732 | if (!MakeTempDir(tmp)) | ||
| 733 | { | ||
| 734 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 735 | return 1; | ||
| 736 | } | ||
| 737 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 738 | { | ||
| 739 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 740 | RemoveTree(tmp); | ||
| 741 | return 1; | ||
| 742 | } | ||
| 743 | |||
| 744 | int nFail = 0; | ||
| 745 | strSetDataDir = tmp; | ||
| 746 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 747 | |||
| 748 | try | ||
| 749 | { | ||
| 750 | const string strPassphrase = "btf-test-passphrase"; | ||
| 751 | const string strWrongPassphrase = "btf-test-passphraser"; | ||
| 752 | |||
| 753 | CKey key; | ||
| 754 | key.MakeNewKey(); | ||
| 755 | vector<unsigned char> vchPubKey = key.GetPubKey(); | ||
| 756 | CPrivKey vchPrivKey = key.GetPrivKey(); | ||
| 757 | |||
| 758 | CKeyingMaterial vchMasterKey; | ||
| 759 | for (int i = 0; i < 32; i++) | ||
| 760 | vchMasterKey.push_back((unsigned char)(i + 1)); | ||
| 761 | |||
| 762 | CWalletMasterKey kMasterKey; | ||
| 763 | kMasterKey.vchSalt.clear(); | ||
| 764 | for (int i = 0; i < 8; i++) | ||
| 765 | kMasterKey.vchSalt.push_back((unsigned char)(0xa0 + i)); | ||
| 766 | kMasterKey.nDeriveIterations = 2500; | ||
| 767 | |||
| 768 | CKeyingMaterial vchPassKey; | ||
| 769 | vector<unsigned char> vchPassIV; | ||
| 770 | nFail += Check(DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, | ||
| 771 | kMasterKey.nDeriveIterations, | ||
| 772 | vchPassKey, vchPassIV), | ||
| 773 | "a passphrase key can be derived") ? 0 : 1; | ||
| 774 | nFail += Check(EncryptSecret(vchPassKey, | ||
| 775 | vector<unsigned char>(vchMasterKey.begin(), vchMasterKey.end()), | ||
| 776 | vchPassIV, kMasterKey.vchCryptedKey), | ||
| 777 | "the wallet master key can be encrypted") ? 0 : 1; | ||
| 778 | |||
| 779 | vector<unsigned char> vchCryptedKey; | ||
| 780 | nFail += Check(EncryptSecret(vchMasterKey, | ||
| 781 | vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()), | ||
| 782 | WalletKeyIV(vchPubKey), vchCryptedKey), | ||
| 783 | "a private key can be encrypted under the wallet master key") ? 0 : 1; | ||
| 784 | |||
| 785 | { | ||
| 786 | CWalletDB walletdb("cr"); | ||
| 787 | nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED), | ||
| 788 | "an encrypted wallet format marker can be written") ? 0 : 1; | ||
| 789 | nFail += Check(walletdb.WriteMasterKey(0, kMasterKey), | ||
| 790 | "the encrypted master key can be written") ? 0 : 1; | ||
| 791 | nFail += Check(walletdb.WriteCryptedKey(vchPubKey, vchCryptedKey), | ||
| 792 | "the encrypted private key can be written") ? 0 : 1; | ||
| 793 | nFail += Check(walletdb.WriteDefaultKey(vchPubKey), | ||
| 794 | "the encrypted wallet default key can be written") ? 0 : 1; | ||
| 795 | } | ||
| 796 | |||
| 797 | ClearWalletRuntimeForTest(); | ||
| 798 | |||
| 799 | vector<unsigned char> vchDefaultKey; | ||
| 800 | nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), | ||
| 801 | "an encrypted wallet can be loaded") ? 0 : 1; | ||
| 802 | nFail += Check(vchDefaultKey == vchPubKey, | ||
| 803 | "the encrypted wallet default public key loads") ? 0 : 1; | ||
| 804 | nFail += Check(IsWalletEncrypted() && IsWalletLocked(), | ||
| 805 | "an encrypted wallet loads locked") ? 0 : 1; | ||
| 806 | nFail += Check(WalletCanSpendKey(vchPubKey), | ||
| 807 | "an encrypted key is recognized as wallet-owned while locked") ? 0 : 1; | ||
| 808 | |||
| 809 | CPrivKey vchDecrypted; | ||
| 810 | string strError; | ||
| 811 | nFail += Check(!GetWalletPrivKey(vchPubKey, vchDecrypted, strError), | ||
| 812 | "a locked encrypted key cannot be read") ? 0 : 1; | ||
| 813 | nFail += Check(!UnlockWallet(strWrongPassphrase, strError), | ||
| 814 | "the wrong-passphrase does not unlock") ? 0 : 1; | ||
| 815 | nFail += Check(UnlockWallet(strPassphrase, strError), | ||
| 816 | "the right passphrase unlocks") ? 0 : 1; | ||
| 817 | nFail += Check(!IsWalletLocked(), | ||
| 818 | "the wallet reports unlocked") ? 0 : 1; | ||
| 819 | nFail += Check(GetWalletPrivKey(vchPubKey, vchDecrypted, strError) && | ||
| 820 | vchDecrypted == vchPrivKey, | ||
| 821 | "an unlocked encrypted key decrypts to the original private key") ? 0 : 1; | ||
| 822 | LockWallet(); | ||
| 823 | nFail += Check(IsWalletLocked(), | ||
| 824 | "locking clears the unlocked state") ? 0 : 1; | ||
| 825 | } | ||
| 826 | catch (const std::exception& e) | ||
| 827 | { | ||
| 828 | printf(" FAIL exception: %s\n", e.what()); | ||
| 829 | nFail++; | ||
| 830 | } | ||
| 831 | catch (...) | ||
| 832 | { | ||
| 833 | printf(" FAIL unknown exception\n"); | ||
| 834 | nFail++; | ||
| 835 | } | ||
| 836 | |||
| 837 | DBFlush(true); | ||
| 838 | SetCurrentDir(cwd); | ||
| 839 | RemoveTree(tmp); | ||
| 840 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 841 | nFail, nFail == 1 ? "" : "s"); | ||
| 842 | fflush(stdout); | ||
| 843 | return nFail == 0 ? 0 : 1; | ||
| 844 | } | ||
| 845 | |||
| 846 | static bool FileContainsBytes(const string& strPath, const vector<unsigned char>& vchNeedle) | ||
| 847 | { | ||
| 848 | if (vchNeedle.empty()) | ||
| 849 | return false; | ||
| 850 | FILE* pf = fopen(strPath.c_str(), "rb"); | ||
| 851 | if (!pf) | ||
| 852 | return false; | ||
| 853 | |||
| 854 | vector<unsigned char> vchHaystack; | ||
| 855 | unsigned char buf[4096]; | ||
| 856 | size_t n = 0; | ||
| 857 | while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) | ||
| 858 | vchHaystack.insert(vchHaystack.end(), buf, buf + n); | ||
| 859 | fclose(pf); | ||
| 860 | |||
| 861 | if (vchHaystack.size() < vchNeedle.size()) | ||
| 862 | return false; | ||
| 863 | return search(vchHaystack.begin(), vchHaystack.end(), | ||
| 864 | vchNeedle.begin(), vchNeedle.end()) != vchHaystack.end(); | ||
| 865 | } | ||
| 866 | |||
| 867 | static bool FileContainsText(const string& strPath, const string& strNeedle) | ||
| 868 | { | ||
| 869 | FILE* pf = fopen(strPath.c_str(), "rb"); | ||
| 870 | if (!pf) | ||
| 871 | return false; | ||
| 872 | |||
| 873 | string strHaystack; | ||
| 874 | char buf[4096]; | ||
| 875 | size_t n = 0; | ||
| 876 | while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) | ||
| 877 | strHaystack.append(buf, buf + n); | ||
| 878 | fclose(pf); | ||
| 879 | |||
| 880 | return strHaystack.find(strNeedle) != string::npos; | ||
| 881 | } | ||
| 882 | |||
| 883 | static bool WriteTextFile(const string& strPath, const string& strText) | ||
| 884 | { | ||
| 885 | FILE* pf = fopen(strPath.c_str(), "wb"); | ||
| 886 | if (!pf) | ||
| 887 | return false; | ||
| 888 | bool fOk = fwrite(strText.data(), 1, strText.size(), pf) == strText.size(); | ||
| 889 | if (fclose(pf) != 0) | ||
| 890 | fOk = false; | ||
| 891 | return fOk; | ||
| 892 | } | ||
| 893 | |||
| 894 | static string QuoteCommandArg(const string& str) | ||
| 895 | { | ||
| 896 | string out = "\""; | ||
| 897 | for (size_t i = 0; i < str.size(); i++) | ||
| 898 | { | ||
| 899 | if (str[i] == '"') | ||
| 900 | out += "\\\""; | ||
| 901 | else | ||
| 902 | out += str[i]; | ||
| 903 | } | ||
| 904 | out += "\""; | ||
| 905 | return out; | ||
| 906 | } | ||
| 907 | |||
| 908 | static int RunBitflashChild(const string& strExe, | ||
| 909 | const vector<string>& vArgs, | ||
| 910 | const string* pStdinFile = NULL) | ||
| 911 | { | ||
| 912 | #ifdef _WIN32 | ||
| 913 | vector<char*> argv; | ||
| 914 | argv.push_back((char*)strExe.c_str()); | ||
| 915 | for (size_t i = 0; i < vArgs.size(); i++) | ||
| 916 | argv.push_back((char*)vArgs[i].c_str()); | ||
| 917 | argv.push_back(NULL); | ||
| 918 | |||
| 919 | int nOldOut = _dup(1); | ||
| 920 | int nOldErr = _dup(2); | ||
| 921 | int nOldIn = _dup(0); | ||
| 922 | int nNull = _open("NUL", _O_WRONLY); | ||
| 923 | int nIn = -1; | ||
| 924 | if (nNull >= 0) | ||
| 925 | { | ||
| 926 | _dup2(nNull, 1); | ||
| 927 | _dup2(nNull, 2); | ||
| 928 | } | ||
| 929 | if (pStdinFile) | ||
| 930 | { | ||
| 931 | nIn = _open(pStdinFile->c_str(), _O_RDONLY); | ||
| 932 | if (nIn >= 0) | ||
| 933 | _dup2(nIn, 0); | ||
| 934 | } | ||
| 935 | int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); | ||
| 936 | if (nIn >= 0) | ||
| 937 | _close(nIn); | ||
| 938 | if (nNull >= 0) | ||
| 939 | _close(nNull); | ||
| 940 | if (nOldIn >= 0) | ||
| 941 | { | ||
| 942 | _dup2(nOldIn, 0); | ||
| 943 | _close(nOldIn); | ||
| 944 | } | ||
| 945 | if (nOldOut >= 0) | ||
| 946 | { | ||
| 947 | _dup2(nOldOut, 1); | ||
| 948 | _close(nOldOut); | ||
| 949 | } | ||
| 950 | if (nOldErr >= 0) | ||
| 951 | { | ||
| 952 | _dup2(nOldErr, 2); | ||
| 953 | _close(nOldErr); | ||
| 954 | } | ||
| 955 | return nRet; | ||
| 956 | #else | ||
| 957 | string strCmd = QuoteCommandArg(strExe); | ||
| 958 | for (size_t i = 0; i < vArgs.size(); i++) | ||
| 959 | strCmd += " " + QuoteCommandArg(vArgs[i]); | ||
| 960 | if (pStdinFile) | ||
| 961 | strCmd += " < " + QuoteCommandArg(*pStdinFile); | ||
| 962 | strCmd += " > /dev/null 2>&1"; | ||
| 963 | return system(strCmd.c_str()); | ||
| 964 | #endif | ||
| 965 | } | ||
| 966 | |||
| 967 | static int RunWalletEncryptSelfTest() | ||
| 968 | { | ||
| 969 | fflush(stdout); | ||
| 970 | printf("wallet-encrypt self-test\n"); | ||
| 971 | |||
| 972 | std::string tmp; | ||
| 973 | std::string cwd; | ||
| 974 | if (!MakeTempDir(tmp)) | ||
| 975 | { | ||
| 976 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 977 | return 1; | ||
| 978 | } | ||
| 979 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 980 | { | ||
| 981 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 982 | RemoveTree(tmp); | ||
| 983 | return 1; | ||
| 984 | } | ||
| 985 | |||
| 986 | int nFail = 0; | ||
| 987 | bool fWalletEnvClosed = false; | ||
| 988 | strSetDataDir = tmp; | ||
| 989 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 990 | |||
| 991 | try | ||
| 992 | { | ||
| 993 | if (!LoadWallet()) | ||
| 994 | throw std::runtime_error("LoadWallet failed"); | ||
| 995 | |||
| 996 | string strError; | ||
| 997 | const string strMnemonic = | ||
| 998 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; | ||
| 999 | nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), | ||
| 1000 | "a phrase can be installed before encryption") ? 0 : 1; | ||
| 1001 | TopUpKeyPool(); | ||
| 1002 | |||
| 1003 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 1004 | CPrivKey vchDefaultPrivKey; | ||
| 1005 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 1006 | "the default private key is readable before encryption") ? 0 : 1; | ||
| 1007 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); | ||
| 1008 | vector<unsigned char> vchPlainHDMaster = vchHDMaster; | ||
| 1009 | vector<unsigned char> vchPlainHDChainCode = vchHDChainCode; | ||
| 1010 | |||
| 1011 | string strBackup; | ||
| 1012 | nFail += Check(EncryptWallet("btf-test-passphrase", strBackup, strError), | ||
| 1013 | "wallet.dat can be rewritten encrypted") ? 0 : 1; | ||
| 1014 | fWalletEnvClosed = true; | ||
| 1015 | nFail += Check(FileExists(strBackup.c_str()), | ||
| 1016 | "the original wallet is preserved as an explicit backup") ? 0 : 1; | ||
| 1017 | |||
| 1018 | string strWalletPath = GetAppDir() + "/wallet.dat"; | ||
| 1019 | nFail += Check(!FileContainsBytes(strWalletPath, vchDefaultPrivBytes), | ||
| 1020 | "the active wallet no longer contains the default private key bytes") ? 0 : 1; | ||
| 1021 | nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDMaster), | ||
| 1022 | "the active wallet no longer contains the HD master key bytes") ? 0 : 1; | ||
| 1023 | nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDChainCode), | ||
| 1024 | "the active wallet no longer contains the HD chain-code bytes") ? 0 : 1; | ||
| 1025 | |||
| 1026 | #ifdef _WIN32 | ||
| 1027 | string strExe = cwd + "\\bitflash.exe"; | ||
| 1028 | #else | ||
| 1029 | string strExe = cwd + "/bitflash-node"; | ||
| 1030 | #endif | ||
| 1031 | string strWrongDump = tmp + "/wrong-pass-dump.txt"; | ||
| 1032 | string strRightDump = tmp + "/right-pass-dump.txt"; | ||
| 1033 | string strLiteralDump = tmp + "/literal-pass-dump.txt"; | ||
| 1034 | string strStdinDump = tmp + "/stdin-pass-dump.txt"; | ||
| 1035 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | ||
| 1036 | string strRightPassFile = tmp + "/right-pass.txt"; | ||
| 1037 | nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && | ||
| 1038 | WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), | ||
| 1039 | "passphrase files can be written for restarted commands") ? 0 : 1; | ||
| 1040 | |||
| 1041 | vector<string> vWrongArgs; | ||
| 1042 | vWrongArgs.push_back("-datadir=" + tmp); | ||
| 1043 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | ||
| 1044 | vWrongArgs.push_back("-dumpwallet=" + strWrongDump); | ||
| 1045 | vWrongArgs.push_back("-nogui"); | ||
| 1046 | int nWrongRet = RunBitflashChild(strExe, vWrongArgs); | ||
| 1047 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | ||
| 1048 | "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; | ||
| 1049 | |||
| 1050 | vector<string> vLiteralArgs; | ||
| 1051 | vLiteralArgs.push_back("-datadir=" + tmp); | ||
| 1052 | vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); | ||
| 1053 | vLiteralArgs.push_back("-dumpwallet=" + strLiteralDump); | ||
| 1054 | vLiteralArgs.push_back("-nogui"); | ||
| 1055 | int nLiteralRet = RunBitflashChild(strExe, vLiteralArgs); | ||
| 1056 | nFail += Check(nLiteralRet != 0 && !FileExists(strLiteralDump.c_str()), | ||
| 1057 | "literal passphrases on the command line are refused") ? 0 : 1; | ||
| 1058 | |||
| 1059 | vector<string> vRightArgs; | ||
| 1060 | vRightArgs.push_back("-datadir=" + tmp); | ||
| 1061 | vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 1062 | vRightArgs.push_back("-dumpwallet=" + strRightDump); | ||
| 1063 | vRightArgs.push_back("-nogui"); | ||
| 1064 | int nRightRet = RunBitflashChild(strExe, vRightArgs); | ||
| 1065 | nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), | ||
| 1066 | "a restarted wallet unlocks with the right passphrase") ? 0 : 1; | ||
| 1067 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | ||
| 1068 | "the restarted wallet can decrypt and dump the original key") ? 0 : 1; | ||
| 1069 | |||
| 1070 | vector<string> vStdinArgs; | ||
| 1071 | vStdinArgs.push_back("-datadir=" + tmp); | ||
| 1072 | vStdinArgs.push_back("-walletpassphrase"); | ||
| 1073 | vStdinArgs.push_back("-dumpwallet=" + strStdinDump); | ||
| 1074 | vStdinArgs.push_back("-nogui"); | ||
| 1075 | int nStdinRet = RunBitflashChild(strExe, vStdinArgs, &strRightPassFile); | ||
| 1076 | nFail += Check(nStdinRet == 0 && FileExists(strStdinDump.c_str()), | ||
| 1077 | "a restarted wallet can read the passphrase from stdin") ? 0 : 1; | ||
| 1078 | } | ||
| 1079 | catch (const std::exception& e) | ||
| 1080 | { | ||
| 1081 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1082 | nFail++; | ||
| 1083 | } | ||
| 1084 | catch (...) | ||
| 1085 | { | ||
| 1086 | printf(" FAIL unknown exception\n"); | ||
| 1087 | nFail++; | ||
| 1088 | } | ||
| 1089 | |||
| 1090 | if (!fWalletEnvClosed) | ||
| 1091 | DBFlush(true); | ||
| 1092 | SetCurrentDir(cwd); | ||
| 1093 | RemoveTree(tmp); | ||
| 1094 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1095 | nFail, nFail == 1 ? "" : "s"); | ||
| 1096 | fflush(stdout); | ||
| 1097 | return nFail == 0 ? 0 : 1; | ||
| 1098 | } | ||
| 1099 | |||
| 1100 | static int RunNetMessageSelfTest() | ||
| 1101 | { | ||
| 1102 | fflush(stdout); | ||
| 1103 | printf("net-message self-test\n"); | ||
| 1104 | |||
| 1105 | int nFail = 0; | ||
| 1106 | try | ||
| 1107 | { | ||
| 1108 | CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 1109 | complete.nVersion = VERSION; | ||
| 1110 | complete.vRecv << CMessageHeader("ping", 0); | ||
| 1111 | nFail += Check(ProcessMessages(&complete), "a complete empty message processes") ? 0 : 1; | ||
| 1112 | nFail += Check(complete.vRecv.empty(), "a complete message is consumed") ? 0 : 1; | ||
| 1113 | nFail += Check(!complete.fDisconnect, "a valid ping does not disconnect the peer") ? 0 : 1; | ||
| 1114 | |||
| 1115 | CNode partial(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 1116 | partial.nVersion = VERSION; | ||
| 1117 | partial.vRecv << CMessageHeader("block", 100); | ||
| 1118 | unsigned int nPartialBefore = partial.vRecv.size(); | ||
| 1119 | nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; | ||
| 1120 | nFail += Check(partial.vRecv.size() == nPartialBefore, | ||
| 1121 | "an incomplete message keeps one header in the buffer") ? 0 : 1; | ||
| 1122 | nFail += Check(partial.nIncompleteMessageStart != 0, | ||
| 1123 | "an incomplete message starts a timeout clock") ? 0 : 1; | ||
| 1124 | nFail += Check(!partial.fDisconnect, | ||
| 1125 | "a fresh incomplete message does not disconnect immediately") ? 0 : 1; | ||
| 1126 | |||
| 1127 | CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 1128 | stale.nVersion = VERSION; | ||
| 1129 | stale.vRecv << CMessageHeader("block", 100); | ||
| 1130 | stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; | ||
| 1131 | stale.nIncompleteMessageSize = 100; | ||
| 1132 | stale.strIncompleteMessageCommand = "block"; | ||
| 1133 | ProcessMessages(&stale); | ||
| 1134 | nFail += Check(stale.fDisconnect, | ||
| 1135 | "a stale incomplete message disconnects the peer") ? 0 : 1; | ||
| 1136 | |||
| 1137 | CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); | ||
| 1138 | oversized.nVersion = VERSION; | ||
| 1139 | oversized.vRecv << CMessageHeader("block", MAX_PROTOCOL_MESSAGE_SIZE + 1); | ||
| 1140 | ProcessMessages(&oversized); | ||
| 1141 | nFail += Check(oversized.fDisconnect, | ||
| 1142 | "an oversized message header disconnects the peer") ? 0 : 1; | ||
| 1143 | } | ||
| 1144 | catch (const std::exception& e) | ||
| 1145 | { | ||
| 1146 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1147 | nFail++; | ||
| 1148 | } | ||
| 1149 | catch (...) | ||
| 1150 | { | ||
| 1151 | printf(" FAIL unknown exception\n"); | ||
| 1152 | nFail++; | ||
| 1153 | } | ||
| 1154 | |||
| 1155 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1156 | nFail, nFail == 1 ? "" : "s"); | ||
| 1157 | fflush(stdout); | ||
| 1158 | return nFail == 0 ? 0 : 1; | ||
| 1159 | } | ||
| 1160 | |||
| 1161 | static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) | ||
| 1162 | { | ||
| 1163 | CTransaction tx; | ||
| 1164 | tx.vin.push_back(CTxIn(COutPoint(), CScript() << 1 << 1)); | ||
| 1165 | tx.vout.push_back(CTxOut(0, CScript())); | ||
| 1166 | tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nScriptBytes, 0); | ||
| 1167 | |||
| 1168 | CBlock block; | ||
| 1169 | block.vtx.push_back(tx); | ||
| 1170 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 1171 | block.nVersion = 1; | ||
| 1172 | block.nTime = GetAdjustedTime(); | ||
| 1173 | block.nBits = bnProofOfWorkLimit.GetCompact(); | ||
| 1174 | block.nNonce = 0; | ||
| 1175 | return block; | ||
| 1176 | } | ||
| 1177 | |||
| 1178 | static int RunConsensusLimitsSelfTest() | ||
| 1179 | { | ||
| 1180 | fflush(stdout); | ||
| 1181 | printf("consensus-limits self-test\n"); | ||
| 1182 | |||
| 1183 | int nFail = 0; | ||
| 1184 | try | ||
| 1185 | { | ||
| 1186 | nFail += Check(MAX_BLOCK_SIZE == 1000000, | ||
| 1187 | "the consensus block-size cap is 1 MB") ? 0 : 1; | ||
| 1188 | nFail += Check(MAX_BLOCK_SIZE < MAX_SIZE, | ||
| 1189 | "the block cap is tighter than the serializer cap") ? 0 : 1; | ||
| 1190 | |||
| 1191 | CBlock small = MakeSizedConsensusBlock(100); | ||
| 1192 | nFail += Check(small.CheckSizeLimits(), | ||
| 1193 | "a small block is within the consensus size limit") ? 0 : 1; | ||
| 1194 | |||
| 1195 | CBlock oversized = MakeSizedConsensusBlock(MAX_BLOCK_SIZE); | ||
| 1196 | unsigned int nSerialized = ::GetSerializeSize(oversized, SER_DISK); | ||
| 1197 | nFail += Check(nSerialized > MAX_BLOCK_SIZE && nSerialized <= MAX_SIZE, | ||
| 1198 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; | ||
| 1199 | nFail += Check(!oversized.CheckSizeLimits(), | ||
| 1200 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; | ||
| 1201 | } | ||
| 1202 | catch (const std::exception& e) | ||
| 1203 | { | ||
| 1204 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1205 | nFail++; | ||
| 1206 | } | ||
| 1207 | catch (...) | ||
| 1208 | { | ||
| 1209 | printf(" FAIL unknown exception\n"); | ||
| 1210 | nFail++; | ||
| 1211 | } | ||
| 1212 | |||
| 1213 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1214 | nFail, nFail == 1 ? "" : "s"); | ||
| 1215 | fflush(stdout); | ||
| 1216 | return nFail == 0 ? 0 : 1; | ||
| 1217 | } | ||
| 1218 | |||
| 1219 | int RunSelfTest(const std::string& name) | ||
| 1220 | { | ||
| 1221 | AttachTerminal(); | ||
| 1222 | |||
| 1223 | if (name == "wallet-keypool") | ||
| 1224 | return RunWalletKeyPoolSelfTest(); | ||
| 1225 | if (name == "wallet-hd") | ||
| 1226 | return RunWalletHDSelfTest(); | ||
| 1227 | if (name == "wallet-format") | ||
| 1228 | return RunWalletFormatSelfTest(); | ||
| 1229 | if (name == "wallet-crypto") | ||
| 1230 | return RunWalletCryptoSelfTest(); | ||
| 1231 | if (name == "wallet-encrypt") | ||
| 1232 | return RunWalletEncryptSelfTest(); | ||
| 1233 | if (name == "net-message") | ||
| 1234 | return RunNetMessageSelfTest(); | ||
| 1235 | if (name == "consensus-limits") | ||
| 1236 | return RunConsensusLimitsSelfTest(); | ||
| 1237 | |||
| 1238 | 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"); | ||
| 1240 | return 1; | ||
| 1241 | } | ||