diff options
| -rw-r--r-- | src/Makefile | 3 | ||||
| -rw-r--r-- | src/db.cpp | 12 | ||||
| -rw-r--r-- | src/db.h | 27 | ||||
| -rw-r--r-- | src/key.h | 34 | ||||
| -rw-r--r-- | src/main.cpp | 112 | ||||
| -rw-r--r-- | src/main.h | 28 | ||||
| -rw-r--r-- | src/main_gui.cpp | 18 | ||||
| -rw-r--r-- | src/makefile.mingw | 3 | ||||
| -rw-r--r-- | src/selftest.cpp | 149 |
9 files changed, 382 insertions, 4 deletions
diff --git a/src/Makefile b/src/Makefile index d4b1837..bbdc9e2 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -50,7 +50,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | |||
| 50 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 50 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 51 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 51 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 52 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ | 52 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 53 | obj/selftest.o | 53 | obj/selftest.o obj/bip32.o |
| 54 | 54 | ||
| 55 | GUI_OBJS = obj/gui.o obj/main_gui.o | 55 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 56 | 56 | ||
| @@ -131,3 +131,4 @@ test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | |||
| 131 | tests: test_bip32 bitflash-node | 131 | tests: test_bip32 bitflash-node |
| 132 | ./test_bip32 | 132 | ./test_bip32 |
| 133 | ./bitflash-node -selftest=wallet-keypool | 133 | ./bitflash-node -selftest=wallet-keypool |
| 134 | ./bitflash-node -selftest=wallet-hd | ||
| @@ -639,6 +639,18 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 639 | { | 639 | { |
| 640 | ssValue >> vchDefaultKeyRet; | 640 | ssValue >> vchDefaultKeyRet; |
| 641 | } | 641 | } |
| 642 | else if (strType == "hdmaster") | ||
| 643 | { | ||
| 644 | ssValue >> vchHDMaster; | ||
| 645 | } | ||
| 646 | else if (strType == "hdchaincode") | ||
| 647 | { | ||
| 648 | ssValue >> vchHDChainCode; | ||
| 649 | } | ||
| 650 | else if (strType == "hdnext") | ||
| 651 | { | ||
| 652 | ssValue >> nHDNext; | ||
| 653 | } | ||
| 642 | else if (strType == "pool") | 654 | else if (strType == "pool") |
| 643 | { | 655 | { |
| 644 | int64 nIndex; | 656 | int64 nIndex; |
| @@ -386,6 +386,33 @@ public: | |||
| 386 | return Write(string("defaultkey"), vchPubKey); | 386 | return Write(string("defaultkey"), vchPubKey); |
| 387 | } | 387 | } |
| 388 | 388 | ||
| 389 | // --- Deterministic (BIP32) seed --------------------------------------- | ||
| 390 | // | ||
| 391 | // Stored as the master key and chain code rather than the mnemonic: the | ||
| 392 | // twelve words are the user's to write down, and keeping a copy of them in | ||
| 393 | // the file they are meant to protect defeats the point of having them. | ||
| 394 | // | ||
| 395 | // "hdnext" is the next child index to derive, so the sequence a restore | ||
| 396 | // reproduces is the same one this wallet handed out. | ||
| 397 | bool ReadHDMaster(vector<unsigned char>& vchMasterRet, vector<unsigned char>& vchChainCodeRet) | ||
| 398 | { | ||
| 399 | vchMasterRet.clear(); | ||
| 400 | vchChainCodeRet.clear(); | ||
| 401 | return Read(string("hdmaster"), vchMasterRet) | ||
| 402 | && Read(string("hdchaincode"), vchChainCodeRet); | ||
| 403 | } | ||
| 404 | |||
| 405 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) | ||
| 406 | { | ||
| 407 | return Write(string("hdmaster"), vchMaster) | ||
| 408 | && Write(string("hdchaincode"), vchChainCode); | ||
| 409 | } | ||
| 410 | |||
| 411 | bool WriteHDNext(unsigned int nNext) | ||
| 412 | { | ||
| 413 | return Write(string("hdnext"), nNext); | ||
| 414 | } | ||
| 415 | |||
| 389 | // Keys generated ahead of time and not yet handed out. The private key of | 416 | // Keys generated ahead of time and not yet handed out. The private key of |
| 390 | // each is already stored under its own "key" record by AddKey; a "pool" | 417 | // each is already stored under its own "key" record by AddKey; a "pool" |
| 391 | // record only says that this one is still unspoken for. | 418 | // record only says that this one is still unspoken for. |
| @@ -86,6 +86,40 @@ public: | |||
| 86 | return true; | 86 | return true; |
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | // Load a raw 32-byte scalar, the form BIP32 derivation produces. | ||
| 90 | // | ||
| 91 | // SetPrivKey above takes DER and cannot be used for this: handing it a | ||
| 92 | // scalar makes d2i_ECPrivateKey fail, and if the caller ignores the return | ||
| 93 | // value -- as one attempt at this did -- the key is left with no material | ||
| 94 | // at all and the address it produces has no private half. Coins paid there | ||
| 95 | // are gone. Hence a separate entry point rather than a looser SetPrivKey. | ||
| 96 | // | ||
| 97 | // The public half is computed here rather than read from anywhere: | ||
| 98 | // pub = G * secret. | ||
| 99 | bool SetSecret(const std::vector<unsigned char>& vchSecret) | ||
| 100 | { | ||
| 101 | if (vchSecret.size() != 32) | ||
| 102 | return false; | ||
| 103 | |||
| 104 | bool fOk = false; | ||
| 105 | BIGNUM* bn = BN_bin2bn(&vchSecret[0], 32, NULL); | ||
| 106 | EC_POINT* pubkey = NULL; | ||
| 107 | if (bn && EC_KEY_set_private_key(pkey, bn)) | ||
| 108 | { | ||
| 109 | const EC_GROUP* group = EC_KEY_get0_group(pkey); | ||
| 110 | pubkey = EC_POINT_new(group); | ||
| 111 | if (pubkey && | ||
| 112 | EC_POINT_mul(group, pubkey, bn, NULL, NULL, NULL) && | ||
| 113 | EC_KEY_set_public_key(pkey, pubkey)) | ||
| 114 | fOk = true; | ||
| 115 | } | ||
| 116 | if (pubkey) | ||
| 117 | EC_POINT_free(pubkey); | ||
| 118 | if (bn) | ||
| 119 | BN_clear_free(bn); // it held the secret | ||
| 120 | return fOk; | ||
| 121 | } | ||
| 122 | |||
| 89 | CPrivKey GetPrivKey() const | 123 | CPrivKey GetPrivKey() const |
| 90 | { | 124 | { |
| 91 | unsigned int nSize = i2d_ECPrivateKey(pkey, NULL); | 125 | unsigned int nSize = i2d_ECPrivateKey(pkey, NULL); |
diff --git a/src/main.cpp b/src/main.cpp index 82a98d2..e5754e6 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <atomic> | 14 | #include <atomic> |
| 15 | #include <mutex> | 15 | #include <mutex> |
| 16 | #include <thread> | 16 | #include <thread> |
| 17 | #include "bip32.h" | ||
| 17 | #include "btfaddr.h" | 18 | #include "btfaddr.h" |
| 18 | #include "btftunnel.h" | 19 | #include "btftunnel.h" |
| 19 | #ifndef _WIN32 | 20 | #ifndef _WIN32 |
| @@ -178,6 +179,66 @@ vector<unsigned char> GenerateNewKey() | |||
| 178 | CCriticalSection cs_keyPool; | 179 | CCriticalSection cs_keyPool; |
| 179 | map<int64, vector<unsigned char> > mapKeyPool; | 180 | map<int64, vector<unsigned char> > mapKeyPool; |
| 180 | 181 | ||
| 182 | vector<unsigned char> vchHDMaster; | ||
| 183 | vector<unsigned char> vchHDChainCode; | ||
| 184 | unsigned int nHDNext = 0; | ||
| 185 | |||
| 186 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) | ||
| 187 | { | ||
| 188 | if (!HaveHDSeed()) | ||
| 189 | { | ||
| 190 | strErrorRet = "no seed"; | ||
| 191 | return false; | ||
| 192 | } | ||
| 193 | |||
| 194 | bitflash::BIP32PrivateNode parent; | ||
| 195 | parent.privateKey = vchHDMaster; | ||
| 196 | parent.chainCode = vchHDChainCode; | ||
| 197 | |||
| 198 | bitflash::BIP32PrivateNode child; | ||
| 199 | if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) | ||
| 200 | return false; | ||
| 201 | |||
| 202 | if (!keyRet.SetSecret(child.privateKey)) | ||
| 203 | { | ||
| 204 | strErrorRet = "derived scalar is not a usable key"; | ||
| 205 | return false; | ||
| 206 | } | ||
| 207 | return true; | ||
| 208 | } | ||
| 209 | |||
| 210 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) | ||
| 211 | { | ||
| 212 | string strNormalized; | ||
| 213 | if (!bitflash::BIP39ValidateMnemonic(strMnemonic, strNormalized, strErrorRet)) | ||
| 214 | return false; | ||
| 215 | |||
| 216 | vector<unsigned char> vchSeed; | ||
| 217 | if (!bitflash::BIP39MnemonicToSeed(strNormalized, "", vchSeed, strErrorRet)) | ||
| 218 | return false; | ||
| 219 | |||
| 220 | bitflash::BIP32PrivateNode master; | ||
| 221 | if (!bitflash::BIP32MasterFromSeed(vchSeed, master, strErrorRet)) | ||
| 222 | return false; | ||
| 223 | |||
| 224 | CRITICAL_BLOCK(cs_keyPool) | ||
| 225 | { | ||
| 226 | // Written before the globals change, so a failed write leaves the | ||
| 227 | // wallet on the seed it already had rather than on one that exists | ||
| 228 | // only in memory. | ||
| 229 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || | ||
| 230 | !CWalletDB().WriteHDNext(0)) | ||
| 231 | { | ||
| 232 | strErrorRet = "could not write the seed to wallet.dat"; | ||
| 233 | return false; | ||
| 234 | } | ||
| 235 | vchHDMaster = master.privateKey; | ||
| 236 | vchHDChainCode = master.chainCode; | ||
| 237 | nHDNext = 0; | ||
| 238 | } | ||
| 239 | return true; | ||
| 240 | } | ||
| 241 | |||
| 181 | void TopUpKeyPool() | 242 | void TopUpKeyPool() |
| 182 | { | 243 | { |
| 183 | CRITICAL_BLOCK(cs_keyPool) | 244 | CRITICAL_BLOCK(cs_keyPool) |
| @@ -191,7 +252,30 @@ void TopUpKeyPool() | |||
| 191 | nIndex = (--mapKeyPool.end())->first + 1; | 252 | nIndex = (--mapKeyPool.end())->first + 1; |
| 192 | 253 | ||
| 193 | CKey key; | 254 | CKey key; |
| 194 | key.MakeNewKey(); | 255 | if (HaveHDSeed()) |
| 256 | { | ||
| 257 | // Derived, so a recovery phrase can reproduce this exact key. | ||
| 258 | // A derivation that fails is not a reason to stop filling the | ||
| 259 | // pool -- fall back to a random key, which is what a wallet | ||
| 260 | // without a seed uses anyway. It will not come back from the | ||
| 261 | // phrase, and that is better than a node that cannot mine. | ||
| 262 | string strError; | ||
| 263 | if (DeriveHDKey(nHDNext, key, strError)) | ||
| 264 | { | ||
| 265 | nHDNext++; | ||
| 266 | CWalletDB().WriteHDNext(nHDNext); | ||
| 267 | } | ||
| 268 | else | ||
| 269 | { | ||
| 270 | printf("TopUpKeyPool() : derivation at index %u failed (%s), " | ||
| 271 | "falling back to a random key\n", nHDNext, strError.c_str()); | ||
| 272 | key.MakeNewKey(); | ||
| 273 | } | ||
| 274 | } | ||
| 275 | else | ||
| 276 | { | ||
| 277 | key.MakeNewKey(); | ||
| 278 | } | ||
| 195 | // AddKey writes the private key. Order matters: the key is in | 279 | // AddKey writes the private key. Order matters: the key is in |
| 196 | // wallet.dat before anything can point at it, so a crash between | 280 | // wallet.dat before anything can point at it, so a crash between |
| 197 | // the two writes costs an unused key, never a usable one. | 281 | // the two writes costs an unused key, never a usable one. |
| @@ -310,6 +394,32 @@ bool AddToWalletIfMine(const CTransaction& tx, const CBlock* pblock) | |||
| 310 | return true; | 394 | return true; |
| 311 | } | 395 | } |
| 312 | 396 | ||
| 397 | bool CanScanWalletTransactions(string& strErrorRet) | ||
| 398 | { | ||
| 399 | strErrorRet.clear(); | ||
| 400 | |||
| 401 | if (pindexGenesisBlock == NULL || pindexBest == NULL || hashBestChain == 0 || nBestHeight < 0) | ||
| 402 | { | ||
| 403 | strErrorRet = "The block index is not loaded, so the wallet scan cannot " | ||
| 404 | "prove whether imported or restored keys own coins."; | ||
| 405 | return false; | ||
| 406 | } | ||
| 407 | |||
| 408 | if (nBestHeight == 0) | ||
| 409 | { | ||
| 410 | strErrorRet = "The local chain is only at height 0. A wallet scan would " | ||
| 411 | "inspect only the genesis block and could incorrectly " | ||
| 412 | "report zero transactions. Start the node and let it sync " | ||
| 413 | "before scanning. If this happened after copying a data " | ||
| 414 | "directory, keep wallet.dat safe and rebuild or re-download " | ||
| 415 | "the block data instead of trusting the copied database/ " | ||
| 416 | "environment."; | ||
| 417 | return false; | ||
| 418 | } | ||
| 419 | |||
| 420 | return true; | ||
| 421 | } | ||
| 422 | |||
| 313 | int ScanForWalletTransactions(CBlockIndex* pindexStart) | 423 | int ScanForWalletTransactions(CBlockIndex* pindexStart) |
| 314 | { | 424 | { |
| 315 | // Walk the chain looking for transactions that belong to keys this wallet | 425 | // Walk the chain looking for transactions that belong to keys this wallet |
| @@ -141,6 +141,33 @@ static const int KEYPOOL_SIZE = 100; | |||
| 141 | extern CCriticalSection cs_keyPool; | 141 | extern CCriticalSection cs_keyPool; |
| 142 | extern map<int64, vector<unsigned char> > mapKeyPool; | 142 | extern map<int64, vector<unsigned char> > mapKeyPool; |
| 143 | 143 | ||
| 144 | // --- Deterministic wallet (BIP32) ----------------------------------------- | ||
| 145 | // | ||
| 146 | // When a seed is present the key pool is derived from it -- m/0'/n, hardened, | ||
| 147 | // n increasing -- instead of being made of random keys. That is what lets a | ||
| 148 | // recovery phrase bring a wallet back: the same twelve words reproduce the same | ||
| 149 | // keys in the same order. | ||
| 150 | // | ||
| 151 | // A wallet without a seed keeps working exactly as before. Nothing here | ||
| 152 | // migrates an existing wallet on its own: the seed is created only when the | ||
| 153 | // user asks for a phrase and confirms they have written it down, because a | ||
| 154 | // phrase generated silently is a backup nobody has. | ||
| 155 | // | ||
| 156 | // Empty until a seed exists. | ||
| 157 | extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL) | ||
| 158 | extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR) | ||
| 159 | extern unsigned int nHDNext; // next child index to derive | ||
| 160 | inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } | ||
| 161 | |||
| 162 | // Install a seed derived from a mnemonic, replacing any existing one, and reset | ||
| 163 | // the derivation counter. Writes to wallet.dat. Returns false and leaves the | ||
| 164 | // wallet untouched if the phrase is not valid. | ||
| 165 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); | ||
| 166 | |||
| 167 | // Derive the child at nIndex and return it as a key. Used by the key pool and | ||
| 168 | // by restore, which needs to run ahead of the pool. | ||
| 169 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); | ||
| 170 | |||
| 144 | // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to | 171 | // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to |
| 145 | // wallet.dat before it is offered to anybody. | 172 | // wallet.dat before it is offered to anybody. |
| 146 | void TopUpKeyPool(); | 173 | void TopUpKeyPool(); |
| @@ -156,6 +183,7 @@ void ReacceptWalletTransactions(); | |||
| 156 | // a key this wallet holds. Returns how many were added or updated. Needed by | 183 | // a key this wallet holds. Returns how many were added or updated. Needed by |
| 157 | // anything that puts a key into the wallet after the fact -- an import, a | 184 | // anything that puts a key into the wallet after the fact -- an import, a |
| 158 | // restored backup, and later a recovery phrase. | 185 | // restored backup, and later a recovery phrase. |
| 186 | bool CanScanWalletTransactions(string& strErrorRet); | ||
| 159 | int ScanForWalletTransactions(CBlockIndex* pindexStart); | 187 | int ScanForWalletTransactions(CBlockIndex* pindexStart); |
| 160 | // Returns how many wallet transactions the chain corrected. | 188 | // Returns how many wallet transactions the chain corrected. |
| 161 | int RescanSpentFlags(); | 189 | int RescanSpentFlags(); |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 2bcdb83..aa9c23d 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -323,6 +323,16 @@ int main(int argc, char* argv[]) | |||
| 323 | // import having failed. | 323 | // import having failed. |
| 324 | if (nAdded > 0) | 324 | if (nAdded > 0) |
| 325 | { | 325 | { |
| 326 | string strScanError; | ||
| 327 | if (!CanScanWalletTransactions(strScanError)) | ||
| 328 | { | ||
| 329 | fprintf(stderr, "Cannot scan for imported coins: %s\n", | ||
| 330 | strScanError.c_str()); | ||
| 331 | fprintf(stderr, "The key import was written, but the wallet " | ||
| 332 | "balance was not proven against the chain.\n"); | ||
| 333 | DBFlush(true); | ||
| 334 | return 1; | ||
| 335 | } | ||
| 326 | printf("Looking through the chain for coins belonging to the imported key(s)...\n"); | 336 | printf("Looking through the chain for coins belonging to the imported key(s)...\n"); |
| 327 | int nFound = ScanForWalletTransactions(pindexGenesisBlock); | 337 | int nFound = ScanForWalletTransactions(pindexGenesisBlock); |
| 328 | printf("Found %d transaction(s). Start the node normally to see the balance.\n", nFound); | 338 | printf("Found %d transaction(s). Start the node normally to see the balance.\n", nFound); |
| @@ -339,6 +349,14 @@ int main(int argc, char* argv[]) | |||
| 339 | // balance is wrong for this reason. | 349 | // balance is wrong for this reason. |
| 340 | if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) | 350 | if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) |
| 341 | { | 351 | { |
| 352 | string strScanError; | ||
| 353 | if (!CanScanWalletTransactions(strScanError)) | ||
| 354 | { | ||
| 355 | fprintf(stderr, "Cannot rescan wallet transactions: %s\n", | ||
| 356 | strScanError.c_str()); | ||
| 357 | DBFlush(true); | ||
| 358 | return 1; | ||
| 359 | } | ||
| 342 | printf("Rescanning the chain for this wallet's transactions...\n"); | 360 | printf("Rescanning the chain for this wallet's transactions...\n"); |
| 343 | int nFound = ScanForWalletTransactions(pindexGenesisBlock); | 361 | int nFound = ScanForWalletTransactions(pindexGenesisBlock); |
| 344 | printf("Rescan done: %d transaction(s) added or updated.\n", nFound); | 362 | printf("Rescan done: %d transaction(s) added or updated.\n", nFound); |
diff --git a/src/makefile.mingw b/src/makefile.mingw index 4f297d9..07ae034 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -42,7 +42,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | |||
| 42 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 42 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 43 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 43 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 44 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ | 44 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 45 | obj/selftest.o | 45 | obj/selftest.o obj/bip32.o |
| 46 | 46 | ||
| 47 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 47 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o |
| 48 | 48 | ||
| @@ -103,3 +103,4 @@ test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | |||
| 103 | tests: test_bip32.exe bitflash.exe | 103 | tests: test_bip32.exe bitflash.exe |
| 104 | ./test_bip32.exe | 104 | ./test_bip32.exe |
| 105 | ./bitflash.exe -selftest=wallet-keypool | 105 | ./bitflash.exe -selftest=wallet-keypool |
| 106 | ./bitflash.exe -selftest=wallet-hd | ||
diff --git a/src/selftest.cpp b/src/selftest.cpp index abe1d15..6792ae7 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -314,14 +314,161 @@ static void AttachTerminal() | |||
| 314 | #endif | 314 | #endif |
| 315 | } | 315 | } |
| 316 | 316 | ||
| 317 | static int RunWalletHDSelfTest() | ||
| 318 | { | ||
| 319 | fflush(stdout); | ||
| 320 | printf("wallet-hd self-test\n"); | ||
| 321 | |||
| 322 | std::string tmp; | ||
| 323 | std::string cwd; | ||
| 324 | if (!MakeTempDir(tmp)) | ||
| 325 | { | ||
| 326 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 327 | return 1; | ||
| 328 | } | ||
| 329 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 330 | { | ||
| 331 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 332 | RemoveTree(tmp); | ||
| 333 | return 1; | ||
| 334 | } | ||
| 335 | |||
| 336 | int nFail = 0; | ||
| 337 | strSetDataDir = tmp; | ||
| 338 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 339 | |||
| 340 | // Test-vector phrases, so nothing here depends on randomness. | ||
| 341 | const std::string strPhraseA = | ||
| 342 | "abandon abandon abandon abandon abandon abandon " | ||
| 343 | "abandon abandon abandon abandon abandon about"; | ||
| 344 | const std::string strPhraseB = | ||
| 345 | "legal winner thank year wave sausage worth useful " | ||
| 346 | "legal winner thank yellow"; | ||
| 347 | |||
| 348 | try | ||
| 349 | { | ||
| 350 | if (!LoadWallet()) | ||
| 351 | throw std::runtime_error("LoadWallet failed"); | ||
| 352 | |||
| 353 | std::string strError; | ||
| 354 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), | ||
| 355 | "a valid phrase installs a seed") ? 0 : 1; | ||
| 356 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; | ||
| 357 | |||
| 358 | std::vector<unsigned char> vchBefore = vchHDMaster; | ||
| 359 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), | ||
| 360 | "an invalid phrase is refused") ? 0 : 1; | ||
| 361 | nFail += Check(vchHDMaster == vchBefore, | ||
| 362 | "a refused phrase leaves the existing seed alone") ? 0 : 1; | ||
| 363 | |||
| 364 | // The property the whole feature exists for: the same words give back | ||
| 365 | // the same keys, in the same order. | ||
| 366 | std::vector<std::string> first; | ||
| 367 | for (unsigned int i = 0; i < 5; i++) | ||
| 368 | { | ||
| 369 | CKey key; | ||
| 370 | if (!DeriveHDKey(i, key, strError)) | ||
| 371 | throw std::runtime_error("derivation failed: " + strError); | ||
| 372 | first.push_back(HexStrLocal(key.GetPubKey())); | ||
| 373 | } | ||
| 374 | |||
| 375 | SetHDSeedFromMnemonic(strPhraseA, strError); | ||
| 376 | std::vector<std::string> again; | ||
| 377 | for (unsigned int i = 0; i < 5; i++) | ||
| 378 | { | ||
| 379 | CKey key; | ||
| 380 | if (!DeriveHDKey(i, key, strError)) | ||
| 381 | throw std::runtime_error("derivation failed: " + strError); | ||
| 382 | again.push_back(HexStrLocal(key.GetPubKey())); | ||
| 383 | } | ||
| 384 | nFail += Check(first == again, | ||
| 385 | "the same phrase derives the same keys in the same order") ? 0 : 1; | ||
| 386 | |||
| 387 | SetHDSeedFromMnemonic(strPhraseB, strError); | ||
| 388 | std::vector<std::string> other; | ||
| 389 | for (unsigned int i = 0; i < 5; i++) | ||
| 390 | { | ||
| 391 | CKey key; | ||
| 392 | if (!DeriveHDKey(i, key, strError)) | ||
| 393 | throw std::runtime_error("derivation failed: " + strError); | ||
| 394 | other.push_back(HexStrLocal(key.GetPubKey())); | ||
| 395 | } | ||
| 396 | nFail += Check(first != other, "a different phrase derives different keys") ? 0 : 1; | ||
| 397 | |||
| 398 | std::set<std::string> distinct(first.begin(), first.end()); | ||
| 399 | nFail += Check(distinct.size() == first.size(), | ||
| 400 | "consecutive indices give distinct keys") ? 0 : 1; | ||
| 401 | |||
| 402 | // With a seed installed the pool must be derived from it, and the | ||
| 403 | // counter must move exactly once per key. | ||
| 404 | SetHDSeedFromMnemonic(strPhraseA, strError); | ||
| 405 | CRITICAL_BLOCK(cs_keyPool) | ||
| 406 | mapKeyPool.clear(); | ||
| 407 | unsigned int nNextBefore = nHDNext; | ||
| 408 | TopUpKeyPool(); | ||
| 409 | |||
| 410 | int nPool = 0; | ||
| 411 | CRITICAL_BLOCK(cs_keyPool) | ||
| 412 | nPool = (int)mapKeyPool.size(); | ||
| 413 | nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; | ||
| 414 | nFail += Check(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, | ||
| 415 | "the derivation counter advances once per pooled key") ? 0 : 1; | ||
| 416 | |||
| 417 | std::set<std::string> derived; | ||
| 418 | for (unsigned int i = nNextBefore; i < nHDNext; i++) | ||
| 419 | { | ||
| 420 | CKey key; | ||
| 421 | if (!DeriveHDKey(i, key, strError)) | ||
| 422 | throw std::runtime_error("derivation failed: " + strError); | ||
| 423 | derived.insert(HexStrLocal(key.GetPubKey())); | ||
| 424 | } | ||
| 425 | |||
| 426 | bool fStored = true; | ||
| 427 | bool fFromSeed = true; | ||
| 428 | CRITICAL_BLOCK(cs_keyPool) | ||
| 429 | { | ||
| 430 | for (map<int64, vector<unsigned char> >::const_iterator mi = mapKeyPool.begin(); | ||
| 431 | mi != mapKeyPool.end(); ++mi) | ||
| 432 | { | ||
| 433 | if (!WalletHasPrivateKey(mi->second)) | ||
| 434 | fStored = false; | ||
| 435 | if (!derived.count(HexStrLocal(mi->second))) | ||
| 436 | fFromSeed = false; | ||
| 437 | } | ||
| 438 | } | ||
| 439 | nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; | ||
| 440 | nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; | ||
| 441 | } | ||
| 442 | catch (const std::exception& e) | ||
| 443 | { | ||
| 444 | printf(" FAIL exception: %s\n", e.what()); | ||
| 445 | nFail++; | ||
| 446 | } | ||
| 447 | catch (...) | ||
| 448 | { | ||
| 449 | printf(" FAIL unknown exception\n"); | ||
| 450 | nFail++; | ||
| 451 | } | ||
| 452 | |||
| 453 | DBFlush(true); | ||
| 454 | SetCurrentDir(cwd); | ||
| 455 | RemoveTree(tmp); | ||
| 456 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 457 | nFail, nFail == 1 ? "" : "s"); | ||
| 458 | fflush(stdout); | ||
| 459 | return nFail == 0 ? 0 : 1; | ||
| 460 | } | ||
| 461 | |||
| 317 | int RunSelfTest(const std::string& name) | 462 | int RunSelfTest(const std::string& name) |
| 318 | { | 463 | { |
| 319 | AttachTerminal(); | 464 | AttachTerminal(); |
| 320 | 465 | ||
| 321 | if (name == "wallet-keypool") | 466 | if (name == "wallet-keypool") |
| 322 | return RunWalletKeyPoolSelfTest(); | 467 | return RunWalletKeyPoolSelfTest(); |
| 468 | if (name == "wallet-hd") | ||
| 469 | return RunWalletHDSelfTest(); | ||
| 323 | 470 | ||
| 324 | printf("Unknown self-test '%s'\n", name.c_str()); | 471 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 325 | printf("Known self-tests: wallet-keypool\n"); | 472 | printf("Known self-tests: wallet-keypool, wallet-hd\n"); |
| 326 | return 1; | 473 | return 1; |
| 327 | } | 474 | } |