diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bip32.cpp | 109 | ||||
| -rw-r--r-- | src/bip32.h | 14 | ||||
| -rw-r--r-- | src/db.cpp | 50 | ||||
| -rw-r--r-- | src/db.h | 27 | ||||
| -rw-r--r-- | src/gui.cpp | 5 | ||||
| -rw-r--r-- | src/main.cpp | 232 | ||||
| -rw-r--r-- | src/main.h | 55 | ||||
| -rw-r--r-- | src/selftest.cpp | 115 | ||||
| -rw-r--r-- | src/test_bip32.cpp | 119 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 117 | ||||
| -rw-r--r-- | src/walletcmd.h | 9 |
11 files changed, 775 insertions, 77 deletions
diff --git a/src/bip32.cpp b/src/bip32.cpp index 6423974..6ba946a 100644 --- a/src/bip32.cpp +++ b/src/bip32.cpp | |||
| @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen, | |||
| 114 | len == 64; | 114 | len == 64; |
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | static bool PrivateKeyToCompressedPubKey(const std::vector<unsigned char>& privateKey, | ||
| 118 | unsigned char out[33], | ||
| 119 | std::string& errorOut) | ||
| 120 | { | ||
| 121 | if (!IsValidPrivateKey(privateKey)) | ||
| 122 | { | ||
| 123 | errorOut = "private key is invalid"; | ||
| 124 | return false; | ||
| 125 | } | ||
| 126 | |||
| 127 | BIGNUM* bn = BN_bin2bn(&privateKey[0], 32, NULL); | ||
| 128 | BN_CTX* ctx = BN_CTX_new(); | ||
| 129 | EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); | ||
| 130 | EC_POINT* pub = group ? EC_POINT_new(group) : NULL; | ||
| 131 | |||
| 132 | bool ok = false; | ||
| 133 | if (bn && ctx && group && pub && | ||
| 134 | EC_POINT_mul(group, pub, bn, NULL, NULL, ctx)) | ||
| 135 | { | ||
| 136 | size_t n = EC_POINT_point2oct(group, pub, POINT_CONVERSION_COMPRESSED, | ||
| 137 | out, 33, ctx); | ||
| 138 | ok = n == 33; | ||
| 139 | } | ||
| 140 | |||
| 141 | BN_free(bn); | ||
| 142 | BN_CTX_free(ctx); | ||
| 143 | EC_POINT_free(pub); | ||
| 144 | EC_GROUP_free(group); | ||
| 145 | |||
| 146 | if (!ok) | ||
| 147 | { | ||
| 148 | errorOut = "could not serialize parent public key"; | ||
| 149 | return false; | ||
| 150 | } | ||
| 151 | return true; | ||
| 152 | } | ||
| 153 | |||
| 117 | bool BIP39EntropyToMnemonic(const std::vector<unsigned char>& entropy, | 154 | bool BIP39EntropyToMnemonic(const std::vector<unsigned char>& entropy, |
| 118 | std::string& mnemonicOut, | 155 | std::string& mnemonicOut, |
| 119 | std::string& errorOut) | 156 | std::string& errorOut) |
| @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& seed, | |||
| 284 | return true; | 321 | return true; |
| 285 | } | 322 | } |
| 286 | 323 | ||
| 287 | bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, | 324 | bool BIP32DeriveChild(const BIP32PrivateNode& parent, |
| 288 | unsigned int childIndex, | 325 | unsigned int childNumber, |
| 289 | BIP32PrivateNode& childOut, | 326 | BIP32PrivateNode& childOut, |
| 290 | std::string& errorOut) | 327 | std::string& errorOut) |
| 291 | { | 328 | { |
| 292 | childOut.privateKey.clear(); | 329 | childOut.privateKey.clear(); |
| 293 | childOut.chainCode.clear(); | 330 | childOut.chainCode.clear(); |
| @@ -297,20 +334,21 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, | |||
| 297 | errorOut = "parent private key or chain code is invalid"; | 334 | errorOut = "parent private key or chain code is invalid"; |
| 298 | return false; | 335 | return false; |
| 299 | } | 336 | } |
| 300 | if (childIndex >= 0x80000000U) | 337 | unsigned char data[37]; |
| 338 | if (childNumber & BIP32_HARDENED) | ||
| 301 | { | 339 | { |
| 302 | errorOut = "child index must be non-hardened; hardening is applied here"; | 340 | data[0] = 0x00; |
| 303 | return false; | 341 | memcpy(data + 1, &parent.privateKey[0], 32); |
| 304 | } | 342 | } |
| 305 | 343 | else | |
| 306 | const unsigned int hardened = childIndex | 0x80000000U; | 344 | { |
| 307 | unsigned char data[37]; | 345 | if (!PrivateKeyToCompressedPubKey(parent.privateKey, data, errorOut)) |
| 308 | data[0] = 0x00; | 346 | return false; |
| 309 | memcpy(data + 1, &parent.privateKey[0], 32); | 347 | } |
| 310 | data[33] = (unsigned char)((hardened >> 24) & 0xff); | 348 | data[33] = (unsigned char)((childNumber >> 24) & 0xff); |
| 311 | data[34] = (unsigned char)((hardened >> 16) & 0xff); | 349 | data[34] = (unsigned char)((childNumber >> 16) & 0xff); |
| 312 | data[35] = (unsigned char)((hardened >> 8) & 0xff); | 350 | data[35] = (unsigned char)((childNumber >> 8) & 0xff); |
| 313 | data[36] = (unsigned char)(hardened & 0xff); | 351 | data[36] = (unsigned char)(childNumber & 0xff); |
| 314 | 352 | ||
| 315 | unsigned char I[64]; | 353 | unsigned char I[64]; |
| 316 | if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) | 354 | if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) |
| @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, | |||
| 363 | return true; | 401 | return true; |
| 364 | } | 402 | } |
| 365 | 403 | ||
| 404 | bool BIP32DerivePath(const BIP32PrivateNode& root, | ||
| 405 | const std::vector<unsigned int>& path, | ||
| 406 | BIP32PrivateNode& nodeOut, | ||
| 407 | std::string& errorOut) | ||
| 408 | { | ||
| 409 | nodeOut.privateKey.clear(); | ||
| 410 | nodeOut.chainCode.clear(); | ||
| 411 | errorOut.clear(); | ||
| 412 | if (!IsValidPrivateKey(root.privateKey) || root.chainCode.size() != 32) | ||
| 413 | { | ||
| 414 | errorOut = "root private key or chain code is invalid"; | ||
| 415 | return false; | ||
| 416 | } | ||
| 417 | |||
| 418 | BIP32PrivateNode cur = root; | ||
| 419 | for (size_t i = 0; i < path.size(); i++) | ||
| 420 | { | ||
| 421 | BIP32PrivateNode next; | ||
| 422 | if (!BIP32DeriveChild(cur, path[i], next, errorOut)) | ||
| 423 | return false; | ||
| 424 | cur = next; | ||
| 425 | } | ||
| 426 | nodeOut = cur; | ||
| 427 | return true; | ||
| 428 | } | ||
| 429 | |||
| 430 | bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, | ||
| 431 | unsigned int childIndex, | ||
| 432 | BIP32PrivateNode& childOut, | ||
| 433 | std::string& errorOut) | ||
| 434 | { | ||
| 435 | if (childIndex >= BIP32_HARDENED) | ||
| 436 | { | ||
| 437 | errorOut = "child index must be non-hardened; hardening is applied here"; | ||
| 438 | return false; | ||
| 439 | } | ||
| 440 | return BIP32DeriveChild(parent, childIndex | BIP32_HARDENED, childOut, errorOut); | ||
| 441 | } | ||
| 442 | |||
| 366 | } // namespace bitflash | 443 | } // namespace bitflash |
diff --git a/src/bip32.h b/src/bip32.h index a029444..e9862d5 100644 --- a/src/bip32.h +++ b/src/bip32.h | |||
| @@ -5,7 +5,7 @@ | |||
| 5 | // | 5 | // |
| 6 | // This module is deliberately independent from wallet.dat and GUI code. It is | 6 | // This module is deliberately independent from wallet.dat and GUI code. It is |
| 7 | // the auditable foundation: mnemonic generation/validation, seed derivation, | 7 | // the auditable foundation: mnemonic generation/validation, seed derivation, |
| 8 | // and hardened private child derivation over secp256k1. | 8 | // and private child derivation over secp256k1. |
| 9 | 9 | ||
| 10 | #ifndef BITFLASH_BIP32_H | 10 | #ifndef BITFLASH_BIP32_H |
| 11 | #define BITFLASH_BIP32_H | 11 | #define BITFLASH_BIP32_H |
| @@ -16,6 +16,8 @@ | |||
| 16 | namespace bitflash | 16 | namespace bitflash |
| 17 | { | 17 | { |
| 18 | 18 | ||
| 19 | static const unsigned int BIP32_HARDENED = 0x80000000U; | ||
| 20 | |||
| 19 | struct BIP32PrivateNode | 21 | struct BIP32PrivateNode |
| 20 | { | 22 | { |
| 21 | std::vector<unsigned char> privateKey; // 32-byte scalar | 23 | std::vector<unsigned char> privateKey; // 32-byte scalar |
| @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& seed, | |||
| 43 | BIP32PrivateNode& nodeOut, | 45 | BIP32PrivateNode& nodeOut, |
| 44 | std::string& errorOut); | 46 | std::string& errorOut); |
| 45 | 47 | ||
| 48 | bool BIP32DeriveChild(const BIP32PrivateNode& parent, | ||
| 49 | unsigned int childNumber, | ||
| 50 | BIP32PrivateNode& childOut, | ||
| 51 | std::string& errorOut); | ||
| 52 | |||
| 53 | bool BIP32DerivePath(const BIP32PrivateNode& root, | ||
| 54 | const std::vector<unsigned int>& path, | ||
| 55 | BIP32PrivateNode& nodeOut, | ||
| 56 | std::string& errorOut); | ||
| 57 | |||
| 46 | bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, | 58 | bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, |
| 47 | unsigned int childIndex, | 59 | unsigned int childIndex, |
| 48 | BIP32PrivateNode& childOut, | 60 | BIP32PrivateNode& childOut, |
| @@ -554,6 +554,7 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 554 | // Whether wallet.dat actually carried a mining mode, as opposed to just the | 554 | // Whether wallet.dat actually carried a mining mode, as opposed to just the |
| 555 | // ancient fGenerateBitcoins flag. See the reconciliation further down. | 555 | // ancient fGenerateBitcoins flag. See the reconciliation further down. |
| 556 | bool fHaveStoredMineMode = false; | 556 | bool fHaveStoredMineMode = false; |
| 557 | bool fHaveStoredHDCoinType = false; | ||
| 557 | vchDefaultKeyRet.clear(); | 558 | vchDefaultKeyRet.clear(); |
| 558 | 559 | ||
| 559 | // Satoshi's "todo: shouldn't we catch exceptions" sat here since 2009, and | 560 | // Satoshi's "todo: shouldn't we catch exceptions" sat here since 2009, and |
| @@ -651,6 +652,23 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 651 | { | 652 | { |
| 652 | ssValue >> nHDNext; | 653 | ssValue >> nHDNext; |
| 653 | } | 654 | } |
| 655 | else if (strType == "hdschema") | ||
| 656 | { | ||
| 657 | ssValue >> nHDKeySchema; | ||
| 658 | } | ||
| 659 | else if (strType == "hdcointype") | ||
| 660 | { | ||
| 661 | ssValue >> nHDCoinType; | ||
| 662 | fHaveStoredHDCoinType = true; | ||
| 663 | } | ||
| 664 | else if (strType == "hdreceivenext") | ||
| 665 | { | ||
| 666 | ssValue >> nHDReceiveNext; | ||
| 667 | } | ||
| 668 | else if (strType == "hdchangenext") | ||
| 669 | { | ||
| 670 | ssValue >> nHDChangeNext; | ||
| 671 | } | ||
| 654 | else if (strType == "pool") | 672 | else if (strType == "pool") |
| 655 | { | 673 | { |
| 656 | int64 nIndex; | 674 | int64 nIndex; |
| @@ -707,6 +725,38 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 707 | return false; | 725 | return false; |
| 708 | } | 726 | } |
| 709 | 727 | ||
| 728 | if (HaveHDSeed()) | ||
| 729 | { | ||
| 730 | if (nHDKeySchema == HD_SCHEMA_NONE) | ||
| 731 | { | ||
| 732 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 733 | printf("LoadWallet: deterministic seed has no schema metadata; " | ||
| 734 | "treating it as legacy m/index'\n"); | ||
| 735 | } | ||
| 736 | else if (nHDKeySchema != HD_SCHEMA_LEGACY && | ||
| 737 | nHDKeySchema != HD_SCHEMA_BIP44) | ||
| 738 | { | ||
| 739 | printf("LoadWallet: deterministic wallet schema %d (%s) is not " | ||
| 740 | "supported by this build\n", | ||
| 741 | nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str()); | ||
| 742 | return false; | ||
| 743 | } | ||
| 744 | if (!fHaveStoredHDCoinType) | ||
| 745 | { | ||
| 746 | nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | ||
| 747 | printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " | ||
| 748 | "using provisional Bitflash coin type %u\n", nHDCoinType); | ||
| 749 | } | ||
| 750 | } | ||
| 751 | else | ||
| 752 | { | ||
| 753 | nHDKeySchema = HD_SCHEMA_NONE; | ||
| 754 | nHDNext = 0; | ||
| 755 | nHDReceiveNext = 0; | ||
| 756 | nHDChangeNext = 0; | ||
| 757 | nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | ||
| 758 | } | ||
| 759 | |||
| 710 | // fGenerateBitcoins and nMineMode only mean anything together, and a | 760 | // fGenerateBitcoins and nMineMode only mean anything together, and a |
| 711 | // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already | 761 | // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already |
| 712 | // wrote fGenerateBitcoins when mining was toggled, years before this fork | 762 | // wrote fGenerateBitcoins when mining was toggled, years before this fork |
| @@ -392,8 +392,11 @@ public: | |||
| 392 | // twelve words are the user's to write down, and keeping a copy of them in | 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. | 393 | // the file they are meant to protect defeats the point of having them. |
| 394 | // | 394 | // |
| 395 | // "hdnext" is the next child index to derive, so the sequence a restore | 395 | // "hdschema" identifies the derivation layout. Wallets created before the |
| 396 | // reproduces is the same one this wallet handed out. | 396 | // field existed are inferred as HD_SCHEMA_LEGACY when a seed is present. |
| 397 | // "hdnext" is that legacy layout's next child index. The receive/change | ||
| 398 | // counters and coin type are reserved for the BIP44 layout that uses | ||
| 399 | // separate chains. | ||
| 397 | bool ReadHDMaster(vector<unsigned char>& vchMasterRet, vector<unsigned char>& vchChainCodeRet) | 400 | bool ReadHDMaster(vector<unsigned char>& vchMasterRet, vector<unsigned char>& vchChainCodeRet) |
| 398 | { | 401 | { |
| 399 | vchMasterRet.clear(); | 402 | vchMasterRet.clear(); |
| @@ -413,6 +416,26 @@ public: | |||
| 413 | return Write(string("hdnext"), nNext); | 416 | return Write(string("hdnext"), nNext); |
| 414 | } | 417 | } |
| 415 | 418 | ||
| 419 | bool WriteHDSchema(int nSchema) | ||
| 420 | { | ||
| 421 | return Write(string("hdschema"), nSchema); | ||
| 422 | } | ||
| 423 | |||
| 424 | bool WriteHDCoinType(unsigned int nCoinType) | ||
| 425 | { | ||
| 426 | return Write(string("hdcointype"), nCoinType); | ||
| 427 | } | ||
| 428 | |||
| 429 | bool WriteHDReceiveNext(unsigned int nNext) | ||
| 430 | { | ||
| 431 | return Write(string("hdreceivenext"), nNext); | ||
| 432 | } | ||
| 433 | |||
| 434 | bool WriteHDChangeNext(unsigned int nNext) | ||
| 435 | { | ||
| 436 | return Write(string("hdchangenext"), nNext); | ||
| 437 | } | ||
| 438 | |||
| 416 | // Keys generated ahead of time and not yet handed out. The private key of | 439 | // Keys generated ahead of time and not yet handed out. The private key of |
| 417 | // each is already stored under its own "key" record by AddKey; a "pool" | 440 | // each is already stored under its own "key" record by AddKey; a "pool" |
| 418 | // record only says that this one is still unspoken for. | 441 | // record only says that this one is still unspoken for. |
diff --git a/src/gui.cpp b/src/gui.cpp index 2335bf9..8f1d204 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -1247,6 +1247,11 @@ static void DrawWalletSafetyDialog() | |||
| 1247 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | 1247 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), |
| 1248 | "No recovery phrase. Only a file backup can rebuild this wallet."); | 1248 | "No recovery phrase. Only a file backup can rebuild this wallet."); |
| 1249 | 1249 | ||
| 1250 | if (g_recoveryAudit.fHaveSeed) | ||
| 1251 | { | ||
| 1252 | ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); | ||
| 1253 | ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); | ||
| 1254 | } | ||
| 1250 | ImGui::Text("Phrase-backed spendable balance: %s BTF", | 1255 | ImGui::Text("Phrase-backed spendable balance: %s BTF", |
| 1251 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); | 1256 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); |
| 1252 | ImGui::Text("Wallet.dat-only spendable balance: %s BTF", | 1257 | ImGui::Text("Wallet.dat-only spendable balance: %s BTF", |
diff --git a/src/main.cpp b/src/main.cpp index 75647ba..efece6c 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -181,7 +181,7 @@ static bool AddKeyIfMissing(const CKey& key) | |||
| 181 | return AddKey(key); | 181 | return AddKey(key); |
| 182 | } | 182 | } |
| 183 | 183 | ||
| 184 | static bool DeriveNextHDWalletKey(vector<unsigned char>& vchPubKeyRet, string& strErrorRet) | 184 | static bool DeriveNextHDReceiveKey(vector<unsigned char>& vchPubKeyRet, string& strErrorRet) |
| 185 | { | 185 | { |
| 186 | vchPubKeyRet.clear(); | 186 | vchPubKeyRet.clear(); |
| 187 | CRITICAL_BLOCK(cs_keyPool) | 187 | CRITICAL_BLOCK(cs_keyPool) |
| @@ -193,7 +193,12 @@ static bool DeriveNextHDWalletKey(vector<unsigned char>& vchPubKeyRet, string& s | |||
| 193 | } | 193 | } |
| 194 | 194 | ||
| 195 | CKey key; | 195 | CKey key; |
| 196 | if (!DeriveHDKey(nHDNext, key, strErrorRet)) | 196 | unsigned int nIndex = 0; |
| 197 | if (nHDKeySchema == HD_SCHEMA_BIP44) | ||
| 198 | nIndex = nHDReceiveNext; | ||
| 199 | else | ||
| 200 | nIndex = nHDNext; | ||
| 201 | if (!DeriveHDKey(nIndex, key, strErrorRet)) | ||
| 197 | return false; | 202 | return false; |
| 198 | if (!AddKeyIfMissing(key)) | 203 | if (!AddKeyIfMissing(key)) |
| 199 | { | 204 | { |
| @@ -201,14 +206,77 @@ static bool DeriveNextHDWalletKey(vector<unsigned char>& vchPubKeyRet, string& s | |||
| 201 | return false; | 206 | return false; |
| 202 | } | 207 | } |
| 203 | 208 | ||
| 204 | unsigned int nNext = nHDNext + 1; | 209 | unsigned int nNext = nIndex + 1; |
| 205 | if (!CWalletDB().WriteHDNext(nNext)) | 210 | if (nHDKeySchema == HD_SCHEMA_BIP44) |
| 211 | { | ||
| 212 | if (!CWalletDB().WriteHDReceiveNext(nNext)) | ||
| 213 | { | ||
| 214 | strErrorRet = "could not advance the receive derivation counter"; | ||
| 215 | return false; | ||
| 216 | } | ||
| 217 | nHDReceiveNext = nNext; | ||
| 218 | } | ||
| 219 | else | ||
| 220 | { | ||
| 221 | if (!CWalletDB().WriteHDNext(nNext)) | ||
| 222 | { | ||
| 223 | strErrorRet = "could not advance the derivation counter"; | ||
| 224 | return false; | ||
| 225 | } | ||
| 226 | nHDNext = nNext; | ||
| 227 | } | ||
| 228 | |||
| 229 | vchPubKeyRet = key.GetPubKey(); | ||
| 230 | } | ||
| 231 | return true; | ||
| 232 | } | ||
| 233 | |||
| 234 | static bool DeriveNextHDChangeKey(vector<unsigned char>& vchPubKeyRet, string& strErrorRet) | ||
| 235 | { | ||
| 236 | vchPubKeyRet.clear(); | ||
| 237 | CRITICAL_BLOCK(cs_keyPool) | ||
| 238 | { | ||
| 239 | if (!HaveHDSeed()) | ||
| 206 | { | 240 | { |
| 207 | strErrorRet = "could not advance the derivation counter"; | 241 | strErrorRet = "no seed"; |
| 208 | return false; | 242 | return false; |
| 209 | } | 243 | } |
| 210 | 244 | ||
| 211 | nHDNext = nNext; | 245 | CKey key; |
| 246 | if (nHDKeySchema == HD_SCHEMA_BIP44) | ||
| 247 | { | ||
| 248 | if (!DeriveHDChangeKey(nHDChangeNext, key, strErrorRet)) | ||
| 249 | return false; | ||
| 250 | if (!AddKeyIfMissing(key)) | ||
| 251 | { | ||
| 252 | strErrorRet = "could not write the derived change key to wallet.dat"; | ||
| 253 | return false; | ||
| 254 | } | ||
| 255 | unsigned int nNext = nHDChangeNext + 1; | ||
| 256 | if (!CWalletDB().WriteHDChangeNext(nNext)) | ||
| 257 | { | ||
| 258 | strErrorRet = "could not advance the change derivation counter"; | ||
| 259 | return false; | ||
| 260 | } | ||
| 261 | nHDChangeNext = nNext; | ||
| 262 | } | ||
| 263 | else | ||
| 264 | { | ||
| 265 | if (!DeriveHDKey(nHDNext, key, strErrorRet)) | ||
| 266 | return false; | ||
| 267 | if (!AddKeyIfMissing(key)) | ||
| 268 | { | ||
| 269 | strErrorRet = "could not write the derived change key to wallet.dat"; | ||
| 270 | return false; | ||
| 271 | } | ||
| 272 | unsigned int nNext = nHDNext + 1; | ||
| 273 | if (!CWalletDB().WriteHDNext(nNext)) | ||
| 274 | { | ||
| 275 | strErrorRet = "could not advance the derivation counter"; | ||
| 276 | return false; | ||
| 277 | } | ||
| 278 | nHDNext = nNext; | ||
| 279 | } | ||
| 212 | vchPubKeyRet = key.GetPubKey(); | 280 | vchPubKeyRet = key.GetPubKey(); |
| 213 | } | 281 | } |
| 214 | return true; | 282 | return true; |
| @@ -229,6 +297,40 @@ map<int64, vector<unsigned char> > mapKeyPool; | |||
| 229 | vector<unsigned char> vchHDMaster; | 297 | vector<unsigned char> vchHDMaster; |
| 230 | vector<unsigned char> vchHDChainCode; | 298 | vector<unsigned char> vchHDChainCode; |
| 231 | unsigned int nHDNext = 0; | 299 | unsigned int nHDNext = 0; |
| 300 | int nHDKeySchema = HD_SCHEMA_NONE; | ||
| 301 | unsigned int nHDReceiveNext = 0; | ||
| 302 | unsigned int nHDChangeNext = 0; | ||
| 303 | unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | ||
| 304 | |||
| 305 | string HDKeySchemaName(int nSchema) | ||
| 306 | { | ||
| 307 | if (nSchema == HD_SCHEMA_LEGACY) | ||
| 308 | return "legacy-hd"; | ||
| 309 | if (nSchema == HD_SCHEMA_BIP44) | ||
| 310 | return "bip44"; | ||
| 311 | return "none"; | ||
| 312 | } | ||
| 313 | |||
| 314 | std::vector<unsigned int> HDLegacyPath(unsigned int nIndex) | ||
| 315 | { | ||
| 316 | std::vector<unsigned int> path; | ||
| 317 | path.push_back(nIndex | bitflash::BIP32_HARDENED); | ||
| 318 | return path; | ||
| 319 | } | ||
| 320 | |||
| 321 | std::vector<unsigned int> HDBIP44Path(unsigned int nCoinType, | ||
| 322 | unsigned int nAccount, | ||
| 323 | unsigned int nChain, | ||
| 324 | unsigned int nIndex) | ||
| 325 | { | ||
| 326 | std::vector<unsigned int> path; | ||
| 327 | path.push_back(HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED); | ||
| 328 | path.push_back(nCoinType | bitflash::BIP32_HARDENED); | ||
| 329 | path.push_back(nAccount | bitflash::BIP32_HARDENED); | ||
| 330 | path.push_back(nChain); | ||
| 331 | path.push_back(nIndex); | ||
| 332 | return path; | ||
| 333 | } | ||
| 232 | 334 | ||
| 233 | static bool ClearKeyPoolRecords(string& strErrorRet) | 335 | static bool ClearKeyPoolRecords(string& strErrorRet) |
| 234 | { | 336 | { |
| @@ -254,13 +356,59 @@ bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) | |||
| 254 | strErrorRet = "no seed"; | 356 | strErrorRet = "no seed"; |
| 255 | return false; | 357 | return false; |
| 256 | } | 358 | } |
| 359 | if (nIndex >= bitflash::BIP32_HARDENED) | ||
| 360 | { | ||
| 361 | strErrorRet = "child index must be non-hardened; hardening is applied here"; | ||
| 362 | return false; | ||
| 363 | } | ||
| 364 | |||
| 365 | bitflash::BIP32PrivateNode parent; | ||
| 366 | parent.privateKey = vchHDMaster; | ||
| 367 | parent.chainCode = vchHDChainCode; | ||
| 368 | |||
| 369 | bitflash::BIP32PrivateNode child; | ||
| 370 | std::vector<unsigned int> path = nHDKeySchema == HD_SCHEMA_BIP44 | ||
| 371 | ? HDBIP44Path(nHDCoinType, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, nIndex) | ||
| 372 | : HDLegacyPath(nIndex); | ||
| 373 | if (!bitflash::BIP32DerivePath(parent, path, child, strErrorRet)) | ||
| 374 | return false; | ||
| 375 | |||
| 376 | if (!keyRet.SetSecret(child.privateKey)) | ||
| 377 | { | ||
| 378 | strErrorRet = "derived scalar is not a usable key"; | ||
| 379 | return false; | ||
| 380 | } | ||
| 381 | return true; | ||
| 382 | } | ||
| 383 | |||
| 384 | bool DeriveHDChangeKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) | ||
| 385 | { | ||
| 386 | if (!HaveHDSeed()) | ||
| 387 | { | ||
| 388 | strErrorRet = "no seed"; | ||
| 389 | return false; | ||
| 390 | } | ||
| 391 | if (nIndex >= bitflash::BIP32_HARDENED) | ||
| 392 | { | ||
| 393 | strErrorRet = "child index must be non-hardened; hardening is applied here"; | ||
| 394 | return false; | ||
| 395 | } | ||
| 396 | |||
| 397 | if (nHDKeySchema != HD_SCHEMA_BIP44) | ||
| 398 | return DeriveHDKey(nIndex, keyRet, strErrorRet); | ||
| 257 | 399 | ||
| 258 | bitflash::BIP32PrivateNode parent; | 400 | bitflash::BIP32PrivateNode parent; |
| 259 | parent.privateKey = vchHDMaster; | 401 | parent.privateKey = vchHDMaster; |
| 260 | parent.chainCode = vchHDChainCode; | 402 | parent.chainCode = vchHDChainCode; |
| 261 | 403 | ||
| 262 | bitflash::BIP32PrivateNode child; | 404 | bitflash::BIP32PrivateNode child; |
| 263 | if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) | 405 | if (!bitflash::BIP32DerivePath(parent, |
| 406 | HDBIP44Path(nHDCoinType, | ||
| 407 | HD_BIP44_ACCOUNT, | ||
| 408 | HD_BIP44_CHAIN_CHANGE, | ||
| 409 | nIndex), | ||
| 410 | child, | ||
| 411 | strErrorRet)) | ||
| 264 | return false; | 412 | return false; |
| 265 | 413 | ||
| 266 | if (!keyRet.SetSecret(child.privateKey)) | 414 | if (!keyRet.SetSecret(child.privateKey)) |
| @@ -307,20 +455,28 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) | |||
| 307 | return false; | 455 | return false; |
| 308 | 456 | ||
| 309 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || | 457 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || |
| 310 | !CWalletDB().WriteHDNext(1)) | 458 | !CWalletDB().WriteHDSchema(HD_SCHEMA_BIP44) || |
| 459 | !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL) || | ||
| 460 | !CWalletDB().WriteHDNext(0) || | ||
| 461 | !CWalletDB().WriteHDReceiveNext(1) || | ||
| 462 | !CWalletDB().WriteHDChangeNext(0)) | ||
| 311 | { | 463 | { |
| 312 | strErrorRet = "could not write the seed to wallet.dat"; | 464 | strErrorRet = "could not write the seed metadata to wallet.dat"; |
| 313 | return false; | 465 | return false; |
| 314 | } | 466 | } |
| 315 | vchHDMaster = master.privateKey; | 467 | vchHDMaster = master.privateKey; |
| 316 | vchHDChainCode = master.chainCode; | 468 | vchHDChainCode = master.chainCode; |
| 469 | nHDKeySchema = HD_SCHEMA_BIP44; | ||
| 470 | nHDReceiveNext = 1; | ||
| 471 | nHDChangeNext = 0; | ||
| 472 | nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | ||
| 317 | 473 | ||
| 318 | // Index 0 is spoken for before it is derived, so a failure below cannot | 474 | // Index 0 is spoken for before it is derived, so a failure below cannot |
| 319 | // leave the pool free to hand it out as an ordinary key. The cost of | 475 | // leave the pool free to hand it out as an ordinary key. The cost of |
| 320 | // the counter being ahead of the derivation is one unused index; the | 476 | // the counter being ahead of the derivation is one unused index; the |
| 321 | // cost of the reverse is the default address and a pool key sharing a | 477 | // cost of the reverse is the default address and a pool key sharing a |
| 322 | // derivation path. | 478 | // derivation path. |
| 323 | nHDNext = 1; | 479 | nHDNext = 0; |
| 324 | 480 | ||
| 325 | // Remember which address stops being the default, so it can be named | 481 | // Remember which address stops being the default, so it can be named |
| 326 | // for what it is. Two entries reading "Your Address" -- one covered by | 482 | // for what it is. Two entries reading "Your Address" -- one covered by |
| @@ -372,15 +528,18 @@ void TopUpKeyPool() | |||
| 372 | // without a seed uses anyway. It will not come back from the | 528 | // without a seed uses anyway. It will not come back from the |
| 373 | // phrase, and that is better than a node that cannot mine. | 529 | // phrase, and that is better than a node that cannot mine. |
| 374 | string strError; | 530 | string strError; |
| 375 | if (DeriveHDKey(nHDNext, key, strError)) | 531 | vector<unsigned char> vchPubKey; |
| 532 | if (DeriveNextHDReceiveKey(vchPubKey, strError)) | ||
| 376 | { | 533 | { |
| 377 | nHDNext++; | 534 | if (!CWalletDB().WritePool(nIndex, vchPubKey)) |
| 378 | CWalletDB().WriteHDNext(nHDNext); | 535 | return; |
| 536 | mapKeyPool[nIndex] = vchPubKey; | ||
| 537 | continue; | ||
| 379 | } | 538 | } |
| 380 | else | 539 | else |
| 381 | { | 540 | { |
| 382 | printf("TopUpKeyPool() : derivation at index %u failed (%s), " | 541 | printf("TopUpKeyPool() : receive derivation failed (%s), " |
| 383 | "falling back to a random key\n", nHDNext, strError.c_str()); | 542 | "falling back to a random key\n", strError.c_str()); |
| 384 | key.MakeNewKey(); | 543 | key.MakeNewKey(); |
| 385 | } | 544 | } |
| 386 | } | 545 | } |
| @@ -3668,11 +3827,20 @@ WalletRecoveryAudit GetWalletRecoveryAudit() | |||
| 3668 | CRITICAL_BLOCK(cs_keyPool) | 3827 | CRITICAL_BLOCK(cs_keyPool) |
| 3669 | { | 3828 | { |
| 3670 | audit.fHaveSeed = HaveHDSeed(); | 3829 | audit.fHaveSeed = HaveHDSeed(); |
| 3830 | audit.nSchema = nHDKeySchema; | ||
| 3671 | audit.nDerivedKnown = nHDNext; | 3831 | audit.nDerivedKnown = nHDNext; |
| 3832 | audit.nReceiveNext = nHDReceiveNext; | ||
| 3833 | audit.nChangeNext = nHDChangeNext; | ||
| 3834 | audit.nCoinType = nHDCoinType; | ||
| 3835 | if (nHDKeySchema == HD_SCHEMA_BIP44) | ||
| 3836 | audit.nDerivedKnown = nHDReceiveNext + nHDChangeNext + nHDNext; | ||
| 3672 | if (audit.fHaveSeed) | 3837 | if (audit.fHaveSeed) |
| 3673 | { | 3838 | { |
| 3674 | string strError; | 3839 | string strError; |
| 3675 | for (unsigned int i = 0; i < nHDNext; i++) | 3840 | unsigned int nReceiveDepth = nHDKeySchema == HD_SCHEMA_BIP44 |
| 3841 | ? nHDReceiveNext | ||
| 3842 | : nHDNext; | ||
| 3843 | for (unsigned int i = 0; i < nReceiveDepth; i++) | ||
| 3676 | { | 3844 | { |
| 3677 | CKey key; | 3845 | CKey key; |
| 3678 | if (!DeriveHDKey(i, key, strError)) | 3846 | if (!DeriveHDKey(i, key, strError)) |
| @@ -3684,6 +3852,36 @@ WalletRecoveryAudit GetWalletRecoveryAudit() | |||
| 3684 | } | 3852 | } |
| 3685 | setDerivedPubKeys.insert(key.GetPubKey()); | 3853 | setDerivedPubKeys.insert(key.GetPubKey()); |
| 3686 | } | 3854 | } |
| 3855 | if (nHDKeySchema == HD_SCHEMA_BIP44) | ||
| 3856 | { | ||
| 3857 | int nSavedSchema = nHDKeySchema; | ||
| 3858 | for (unsigned int i = 0; i < nHDChangeNext; i++) | ||
| 3859 | { | ||
| 3860 | CKey key; | ||
| 3861 | if (!DeriveHDChangeKey(i, key, strError)) | ||
| 3862 | { | ||
| 3863 | audit.fDeriveComplete = false; | ||
| 3864 | audit.strDeriveError = strprintf("change derivation failed at index %u: %s", | ||
| 3865 | i, strError.c_str()); | ||
| 3866 | break; | ||
| 3867 | } | ||
| 3868 | setDerivedPubKeys.insert(key.GetPubKey()); | ||
| 3869 | } | ||
| 3870 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 3871 | for (unsigned int i = 0; i < nHDNext; i++) | ||
| 3872 | { | ||
| 3873 | CKey key; | ||
| 3874 | if (!DeriveHDKey(i, key, strError)) | ||
| 3875 | { | ||
| 3876 | audit.fDeriveComplete = false; | ||
| 3877 | audit.strDeriveError = strprintf("legacy compatibility derivation failed at index %u: %s", | ||
| 3878 | i, strError.c_str()); | ||
| 3879 | break; | ||
| 3880 | } | ||
| 3881 | setDerivedPubKeys.insert(key.GetPubKey()); | ||
| 3882 | } | ||
| 3883 | nHDKeySchema = nSavedSchema; | ||
| 3884 | } | ||
| 3687 | } | 3885 | } |
| 3688 | } | 3886 | } |
| 3689 | 3887 | ||
| @@ -3926,7 +4124,7 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in | |||
| 3926 | if (HaveHDSeed()) | 4124 | if (HaveHDSeed()) |
| 3927 | { | 4125 | { |
| 3928 | string strError; | 4126 | string strError; |
| 3929 | if (!DeriveNextHDWalletKey(vchPubKey, strError)) | 4127 | if (!DeriveNextHDChangeKey(vchPubKey, strError)) |
| 3930 | { | 4128 | { |
| 3931 | printf("CreateTransaction() : could not derive a phrase-backed change key: %s\n", | 4129 | printf("CreateTransaction() : could not derive a phrase-backed change key: %s\n", |
| 3932 | strError.c_str()); | 4130 | strError.c_str()); |
| @@ -147,21 +147,45 @@ extern map<int64, vector<unsigned char> > mapKeyPool; | |||
| 147 | 147 | ||
| 148 | // --- Deterministic wallet (BIP32) ----------------------------------------- | 148 | // --- Deterministic wallet (BIP32) ----------------------------------------- |
| 149 | // | 149 | // |
| 150 | // When a seed is present the key pool is derived from it -- m/0'/n, hardened, | 150 | // When a seed is present today, the key pool is derived from it by the legacy |
| 151 | // n increasing -- instead of being made of random keys. That is what lets a | 151 | // Bitflash path -- m/index', hardened, index increasing -- instead of being |
| 152 | // recovery phrase bring a wallet back: the same twelve words reproduce the same | 152 | // made of random keys. That is what lets a recovery phrase bring a wallet back: |
| 153 | // keys in the same order. | 153 | // the same twelve words reproduce the same keys in the same order. |
| 154 | // | 154 | // |
| 155 | // A wallet without a seed keeps working exactly as before. Nothing here | 155 | // BIP44 uses a different path family and separate receive/change counters. |
| 156 | // migrates an existing wallet on its own: the seed is created only when the | 156 | // The schema fields below let new wallets opt into that without making old |
| 157 | // user asks for a phrase and confirms they have written it down, because a | 157 | // m/index' coins disappear. |
| 158 | // phrase generated silently is a backup nobody has. | 158 | // |
| 159 | // A wallet without a seed keeps working exactly as before. Existing seeded | ||
| 160 | // wallets keep their recorded schema. A new phrase uses BIP44, but it is still | ||
| 161 | // created only when the user asks for a phrase and confirms they have written | ||
| 162 | // it down, because a phrase generated silently is a backup nobody has. | ||
| 159 | // | 163 | // |
| 160 | // Empty until a seed exists. | 164 | // Empty until a seed exists. |
| 161 | extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL) | 165 | extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL) |
| 162 | extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR) | 166 | extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR) |
| 163 | extern unsigned int nHDNext; // next child index to derive | 167 | extern unsigned int nHDNext; // next child index to derive |
| 168 | static const int HD_SCHEMA_NONE = 0; | ||
| 169 | static const int HD_SCHEMA_LEGACY = 1; // m/index' | ||
| 170 | static const int HD_SCHEMA_BIP44 = 2; // m/44'/coin_type'/account'/change/index | ||
| 171 | static const unsigned int HD_BIP44_PURPOSE = 44; | ||
| 172 | static const unsigned int HD_BIP44_ACCOUNT = 0; | ||
| 173 | static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; | ||
| 174 | static const unsigned int HD_BIP44_CHAIN_CHANGE = 1; | ||
| 175 | // Provisional until Bitflash has an official SLIP-0044 assignment. | ||
| 176 | // Proposed registry row: 4346950 | BITFLASH | Bitflash. | ||
| 177 | static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950; | ||
| 178 | extern int nHDKeySchema; | ||
| 179 | extern unsigned int nHDReceiveNext; // BIP44 external chain | ||
| 180 | extern unsigned int nHDChangeNext; // BIP44 internal chain | ||
| 181 | extern unsigned int nHDCoinType; // BIP44 coin_type' | ||
| 164 | inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } | 182 | inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } |
| 183 | string HDKeySchemaName(int nSchema); | ||
| 184 | std::vector<unsigned int> HDLegacyPath(unsigned int nIndex); | ||
| 185 | std::vector<unsigned int> HDBIP44Path(unsigned int nCoinType, | ||
| 186 | unsigned int nAccount, | ||
| 187 | unsigned int nChain, | ||
| 188 | unsigned int nIndex); | ||
| 165 | 189 | ||
| 166 | // Install a seed derived from a mnemonic, replacing any existing one. The | 190 | // Install a seed derived from a mnemonic, replacing any existing one. The |
| 167 | // default receiving key is derived immediately, so the next visible address is | 191 | // default receiving key is derived immediately, so the next visible address is |
| @@ -169,9 +193,12 @@ inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.siz | |||
| 169 | // wallet untouched if the phrase is not valid. | 193 | // wallet untouched if the phrase is not valid. |
| 170 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); | 194 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); |
| 171 | 195 | ||
| 172 | // Derive the child at nIndex and return it as a key. Used by the key pool and | 196 | // Derive the receiving child at nIndex for the wallet's active schema and |
| 173 | // by restore, which needs to run ahead of the pool. | 197 | // return it as a key. Legacy seeded wallets use m/index'. BIP44 wallets use |
| 198 | // m/44'/coin_type'/0'/0/index. Used by the key pool and restore, which needs | ||
| 199 | // to run ahead of the pool. | ||
| 174 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); | 200 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); |
| 201 | bool DeriveHDChangeKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); | ||
| 175 | 202 | ||
| 176 | // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to | 203 | // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to |
| 177 | // wallet.dat before it is offered to anybody. | 204 | // wallet.dat before it is offered to anybody. |
| @@ -221,7 +248,11 @@ struct WalletRecoveryAudit | |||
| 221 | { | 248 | { |
| 222 | bool fHaveSeed; | 249 | bool fHaveSeed; |
| 223 | bool fDeriveComplete; | 250 | bool fDeriveComplete; |
| 251 | int nSchema; | ||
| 224 | unsigned int nDerivedKnown; | 252 | unsigned int nDerivedKnown; |
| 253 | unsigned int nReceiveNext; | ||
| 254 | unsigned int nChangeNext; | ||
| 255 | unsigned int nCoinType; | ||
| 225 | int nRecoverableTx; | 256 | int nRecoverableTx; |
| 226 | int nLegacyTx; | 257 | int nLegacyTx; |
| 227 | int nRecoverableImmatureTx; | 258 | int nRecoverableImmatureTx; |
| @@ -236,7 +267,11 @@ struct WalletRecoveryAudit | |||
| 236 | { | 267 | { |
| 237 | fHaveSeed = false; | 268 | fHaveSeed = false; |
| 238 | fDeriveComplete = true; | 269 | fDeriveComplete = true; |
| 270 | nSchema = HD_SCHEMA_NONE; | ||
| 239 | nDerivedKnown = 0; | 271 | nDerivedKnown = 0; |
| 272 | nReceiveNext = 0; | ||
| 273 | nChangeNext = 0; | ||
| 274 | nCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | ||
| 240 | nRecoverableTx = 0; | 275 | nRecoverableTx = 0; |
| 241 | nLegacyTx = 0; | 276 | nLegacyTx = 0; |
| 242 | nRecoverableImmatureTx = 0; | 277 | nRecoverableImmatureTx = 0; |
diff --git a/src/selftest.cpp b/src/selftest.cpp index df55bcd..aee2951 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -6,7 +6,9 @@ | |||
| 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 "bip32.h" | ||
| 9 | #include "selftest.h" | 10 | #include "selftest.h" |
| 11 | #include "walletcmd.h" | ||
| 10 | 12 | ||
| 11 | // Test results go to the terminal, not to debug.log. | 13 | // Test results go to the terminal, not to debug.log. |
| 12 | // | 14 | // |
| @@ -342,18 +344,68 @@ static int RunWalletHDSelfTest() | |||
| 342 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), | 344 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), |
| 343 | "a valid phrase installs a seed") ? 0 : 1; | 345 | "a valid phrase installs a seed") ? 0 : 1; |
| 344 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; | 346 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; |
| 347 | nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, | ||
| 348 | "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; | ||
| 349 | nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 350 | "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; | ||
| 351 | nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, | ||
| 352 | "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; | ||
| 353 | std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 354 | HD_BIP44_ACCOUNT, | ||
| 355 | HD_BIP44_CHAIN_RECEIVE, | ||
| 356 | 0); | ||
| 357 | nFail += Check(vBIP44Path.size() == 5 && | ||
| 358 | vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && | ||
| 359 | vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && | ||
| 360 | vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && | ||
| 361 | vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && | ||
| 362 | vBIP44Path[4] == 0, | ||
| 363 | "the BIP44 helper builds m/44'/coin_type'/account'/change/index") ? 0 : 1; | ||
| 345 | 364 | ||
| 346 | int nPoolAfterSeed = 0; | 365 | int nPoolAfterSeed = 0; |
| 347 | CRITICAL_BLOCK(cs_keyPool) | 366 | CRITICAL_BLOCK(cs_keyPool) |
| 348 | nPoolAfterSeed = (int)mapKeyPool.size(); | 367 | nPoolAfterSeed = (int)mapKeyPool.size(); |
| 349 | nFail += Check(nPoolAfterSeed == 0, | 368 | nFail += Check(nPoolAfterSeed == 0, |
| 350 | "installing a phrase clears the old random key pool") ? 0 : 1; | 369 | "installing a phrase clears the old random key pool") ? 0 : 1; |
| 351 | nFail += Check(nHDNext == 1, | 370 | nFail += Check(nHDNext == 0, |
| 352 | "installing a phrase reserves one derived default key") ? 0 : 1; | 371 | "installing a BIP44 phrase leaves the legacy counter unused") ? 0 : 1; |
| 353 | 372 | ||
| 354 | CKey keyFirstDerived; | 373 | CKey keyFirstDerived; |
| 355 | if (!DeriveHDKey(0, keyFirstDerived, strError)) | 374 | if (!DeriveHDKey(0, keyFirstDerived, strError)) |
| 356 | throw std::runtime_error("default derivation failed: " + strError); | 375 | throw std::runtime_error("default derivation failed: " + strError); |
| 376 | bitflash::BIP32PrivateNode hdParent; | ||
| 377 | hdParent.privateKey = vchHDMaster; | ||
| 378 | hdParent.chainCode = vchHDChainCode; | ||
| 379 | bitflash::BIP32PrivateNode hdBIP44ReceiveChild; | ||
| 380 | if (!bitflash::BIP32DerivePath(hdParent, | ||
| 381 | HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 382 | HD_BIP44_ACCOUNT, | ||
| 383 | HD_BIP44_CHAIN_RECEIVE, | ||
| 384 | 0), | ||
| 385 | hdBIP44ReceiveChild, | ||
| 386 | strError)) | ||
| 387 | throw std::runtime_error("BIP44 receive path derivation failed: " + strError); | ||
| 388 | CKey keyBIP44ReceivePath; | ||
| 389 | if (!keyBIP44ReceivePath.SetSecret(hdBIP44ReceiveChild.privateKey)) | ||
| 390 | throw std::runtime_error("BIP44 receive path produced an unusable key"); | ||
| 391 | nFail += Check(keyBIP44ReceivePath.GetPubKey() == keyFirstDerived.GetPubKey(), | ||
| 392 | "the default HD key path is m/44'/coin_type'/0'/0/0") ? 0 : 1; | ||
| 393 | |||
| 394 | bitflash::BIP32PrivateNode hdLegacyChild; | ||
| 395 | int nSchemaForLegacyCheck = nHDKeySchema; | ||
| 396 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 397 | CKey keyLegacyDerived; | ||
| 398 | if (!DeriveHDKey(0, keyLegacyDerived, strError)) | ||
| 399 | throw std::runtime_error("legacy derivation failed: " + strError); | ||
| 400 | nHDKeySchema = nSchemaForLegacyCheck; | ||
| 401 | if (!bitflash::BIP32DerivePath(hdParent, HDLegacyPath(0), hdLegacyChild, strError)) | ||
| 402 | throw std::runtime_error("legacy path derivation failed: " + strError); | ||
| 403 | CKey keyLegacyPath; | ||
| 404 | if (!keyLegacyPath.SetSecret(hdLegacyChild.privateKey)) | ||
| 405 | throw std::runtime_error("legacy path produced an unusable key"); | ||
| 406 | nFail += Check(keyLegacyPath.GetPubKey() == keyLegacyDerived.GetPubKey(), | ||
| 407 | "the legacy HD key path remains m/index'") ? 0 : 1; | ||
| 408 | |||
| 357 | std::vector<unsigned char> vchDefaultKey; | 409 | std::vector<unsigned char> vchDefaultKey; |
| 358 | bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey); | 410 | bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey); |
| 359 | nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(), | 411 | nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(), |
| @@ -370,10 +422,16 @@ static int RunWalletHDSelfTest() | |||
| 370 | "the pre-seed address is kept, named apart from the new one") ? 0 : 1; | 422 | "the pre-seed address is kept, named apart from the new one") ? 0 : 1; |
| 371 | 423 | ||
| 372 | std::vector<unsigned char> vchBefore = vchHDMaster; | 424 | std::vector<unsigned char> vchBefore = vchHDMaster; |
| 425 | int nSchemaBefore = nHDKeySchema; | ||
| 426 | unsigned int nCoinTypeBefore = nHDCoinType; | ||
| 373 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), | 427 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), |
| 374 | "an invalid phrase is refused") ? 0 : 1; | 428 | "an invalid phrase is refused") ? 0 : 1; |
| 375 | nFail += Check(vchHDMaster == vchBefore, | 429 | nFail += Check(vchHDMaster == vchBefore, |
| 376 | "a refused phrase leaves the existing seed alone") ? 0 : 1; | 430 | "a refused phrase leaves the existing seed alone") ? 0 : 1; |
| 431 | nFail += Check(nHDKeySchema == nSchemaBefore, | ||
| 432 | "a refused phrase leaves the derivation schema alone") ? 0 : 1; | ||
| 433 | nFail += Check(nHDCoinType == nCoinTypeBefore, | ||
| 434 | "a refused phrase leaves the BIP44 coin type alone") ? 0 : 1; | ||
| 377 | 435 | ||
| 378 | // The property the whole feature exists for: the same words give back | 436 | // The property the whole feature exists for: the same words give back |
| 379 | // the same keys, in the same order. | 437 | // the same keys, in the same order. |
| @@ -416,18 +474,26 @@ static int RunWalletHDSelfTest() | |||
| 416 | // With a seed installed the pool must be derived from it, and the | 474 | // With a seed installed the pool must be derived from it, and the |
| 417 | // counter must move exactly once per key. | 475 | // counter must move exactly once per key. |
| 418 | SetHDSeedFromMnemonic(strPhraseA, strError); | 476 | SetHDSeedFromMnemonic(strPhraseA, strError); |
| 419 | unsigned int nNextBefore = nHDNext; | 477 | unsigned int nReceiveNextBefore = nHDReceiveNext; |
| 420 | TopUpKeyPool(); | 478 | TopUpKeyPool(); |
| 421 | 479 | ||
| 422 | int nPool = 0; | 480 | int nPool = 0; |
| 423 | CRITICAL_BLOCK(cs_keyPool) | 481 | CRITICAL_BLOCK(cs_keyPool) |
| 424 | nPool = (int)mapKeyPool.size(); | 482 | nPool = (int)mapKeyPool.size(); |
| 425 | nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; | 483 | nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; |
| 426 | nFail += Check(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, | 484 | nFail += Check(nHDReceiveNext == nReceiveNextBefore + (unsigned int)KEYPOOL_SIZE, |
| 427 | "the derivation counter advances once per pooled key") ? 0 : 1; | 485 | "the BIP44 receive counter advances once per pooled key") ? 0 : 1; |
| 486 | nFail += Check(nHDChangeNext == 0, | ||
| 487 | "filling the receive pool leaves the BIP44 change counter alone") ? 0 : 1; | ||
| 488 | nFail += Check(!RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 400, 600, 600), | ||
| 489 | "BIP44 restore depth is not satisfied by receive plus legacy alone") ? 0 : 1; | ||
| 490 | nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 600, 600, 600), | ||
| 491 | "BIP44 restore depth is satisfied on each branch") ? 0 : 1; | ||
| 492 | nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_LEGACY, 0, 0, 600, 600), | ||
| 493 | "legacy restore depth still follows the legacy counter") ? 0 : 1; | ||
| 428 | 494 | ||
| 429 | std::set<std::string> derived; | 495 | std::set<std::string> derived; |
| 430 | for (unsigned int i = nNextBefore; i < nHDNext; i++) | 496 | for (unsigned int i = nReceiveNextBefore; i < nHDReceiveNext; i++) |
| 431 | { | 497 | { |
| 432 | CKey key; | 498 | CKey key; |
| 433 | if (!DeriveHDKey(i, key, strError)) | 499 | if (!DeriveHDKey(i, key, strError)) |
| @@ -456,11 +522,32 @@ static int RunWalletHDSelfTest() | |||
| 456 | CKey keyAuditDerived; | 522 | CKey keyAuditDerived; |
| 457 | if (!DeriveHDKey(0, keyAuditDerived, strError)) | 523 | if (!DeriveHDKey(0, keyAuditDerived, strError)) |
| 458 | throw std::runtime_error("audit derivation failed: " + strError); | 524 | throw std::runtime_error("audit derivation failed: " + strError); |
| 525 | CKey keyAuditChange; | ||
| 526 | if (!DeriveHDChangeKey(0, keyAuditChange, strError)) | ||
| 527 | throw std::runtime_error("audit change derivation failed: " + strError); | ||
| 528 | nFail += Check(keyAuditChange.GetPubKey() != keyAuditDerived.GetPubKey(), | ||
| 529 | "BIP44 receive and change chains derive different keys") ? 0 : 1; | ||
| 530 | if (!AddKey(keyAuditChange)) | ||
| 531 | throw std::runtime_error("could not store the audit change key"); | ||
| 532 | nHDChangeNext = 1; | ||
| 533 | int nSchemaForLegacyAudit = nHDKeySchema; | ||
| 534 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 535 | CKey keyAuditLegacyHD; | ||
| 536 | if (!DeriveHDKey(0, keyAuditLegacyHD, strError)) | ||
| 537 | throw std::runtime_error("audit legacy compatibility derivation failed: " + strError); | ||
| 538 | nHDKeySchema = nSchemaForLegacyAudit; | ||
| 539 | if (!AddKey(keyAuditLegacyHD)) | ||
| 540 | throw std::runtime_error("could not store the audit legacy compatibility key"); | ||
| 541 | nHDNext = 1; | ||
| 459 | 542 | ||
| 460 | CWalletTx wtxLegacy; | 543 | CWalletTx wtxLegacy; |
| 461 | wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | 544 | wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); |
| 462 | CWalletTx wtxDerived; | 545 | CWalletTx wtxDerived; |
| 463 | wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); | 546 | wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); |
| 547 | CWalletTx wtxChange; | ||
| 548 | wtxChange.vout.push_back(CTxOut(13 * COIN, CScript() << keyAuditChange.GetPubKey() << OP_CHECKSIG)); | ||
| 549 | CWalletTx wtxLegacyHD; | ||
| 550 | wtxLegacyHD.vout.push_back(CTxOut(17 * COIN, CScript() << keyAuditLegacyHD.GetPubKey() << OP_CHECKSIG)); | ||
| 464 | CWalletTx wtxImmatureLegacy; | 551 | CWalletTx wtxImmatureLegacy; |
| 465 | wtxImmatureLegacy.vin.push_back(CTxIn()); | 552 | wtxImmatureLegacy.vin.push_back(CTxIn()); |
| 466 | wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | 553 | wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); |
| @@ -470,17 +557,25 @@ static int RunWalletHDSelfTest() | |||
| 470 | mapWallet.clear(); | 557 | mapWallet.clear(); |
| 471 | mapWallet[wtxLegacy.GetHash()] = wtxLegacy; | 558 | mapWallet[wtxLegacy.GetHash()] = wtxLegacy; |
| 472 | mapWallet[wtxDerived.GetHash()] = wtxDerived; | 559 | mapWallet[wtxDerived.GetHash()] = wtxDerived; |
| 560 | mapWallet[wtxChange.GetHash()] = wtxChange; | ||
| 561 | mapWallet[wtxLegacyHD.GetHash()] = wtxLegacyHD; | ||
| 473 | mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; | 562 | mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; |
| 474 | } | 563 | } |
| 475 | 564 | ||
| 476 | WalletRecoveryAudit audit = GetWalletRecoveryAudit(); | 565 | WalletRecoveryAudit audit = GetWalletRecoveryAudit(); |
| 477 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; | 566 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; |
| 567 | nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, | ||
| 568 | "the recovery audit reports the derivation schema") ? 0 : 1; | ||
| 569 | nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, | ||
| 570 | "the recovery audit reports the BIP44 coin type") ? 0 : 1; | ||
| 571 | nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, | ||
| 572 | "the recovery audit reports receive/change counters") ? 0 : 1; | ||
| 478 | nFail += Check(audit.nLegacyCredit == 5 * COIN, | 573 | nFail += Check(audit.nLegacyCredit == 5 * COIN, |
| 479 | "the recovery audit finds wallet.dat-only balance") ? 0 : 1; | 574 | "the recovery audit finds wallet.dat-only balance") ? 0 : 1; |
| 480 | nFail += Check(audit.nRecoverableCredit == 7 * COIN, | 575 | nFail += Check(audit.nRecoverableCredit == 37 * COIN, |
| 481 | "the recovery audit finds phrase-backed balance") ? 0 : 1; | 576 | "the recovery audit finds BIP44 and legacy-HD phrase balance") ? 0 : 1; |
| 482 | nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 1, | 577 | nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 3, |
| 483 | "the recovery audit counts legacy and phrase-backed transactions") ? 0 : 1; | 578 | "the recovery audit counts wallet.dat-only and phrase-backed transactions") ? 0 : 1; |
| 484 | nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, | 579 | nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, |
| 485 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; | 580 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; |
| 486 | nFail += Check(audit.nLegacyImmatureTx == 1, | 581 | nFail += Check(audit.nLegacyImmatureTx == 1, |
diff --git a/src/test_bip32.cpp b/src/test_bip32.cpp index 610a651..6c11510 100644 --- a/src/test_bip32.cpp +++ b/src/test_bip32.cpp | |||
| @@ -5,6 +5,8 @@ | |||
| 5 | 5 | ||
| 6 | #include "bip32.h" | 6 | #include "bip32.h" |
| 7 | 7 | ||
| 8 | #include <openssl/sha.h> | ||
| 9 | |||
| 8 | #include <cstdio> | 10 | #include <cstdio> |
| 9 | #include <cstring> | 11 | #include <cstring> |
| 10 | #include <string> | 12 | #include <string> |
| @@ -57,6 +59,76 @@ static std::string ToHex(const std::vector<unsigned char>& v) | |||
| 57 | return out; | 59 | return out; |
| 58 | } | 60 | } |
| 59 | 61 | ||
| 62 | static bool DecodeBase58Check(const std::string& str, std::vector<unsigned char>& out) | ||
| 63 | { | ||
| 64 | static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; | ||
| 65 | out.clear(); | ||
| 66 | |||
| 67 | std::vector<unsigned char> b256(str.size() * 733 / 1000 + 1); | ||
| 68 | for (char c : str) | ||
| 69 | { | ||
| 70 | const char* p = strchr(pszBase58, c); | ||
| 71 | if (!p) | ||
| 72 | return false; | ||
| 73 | int carry = (int)(p - pszBase58); | ||
| 74 | for (std::vector<unsigned char>::reverse_iterator it = b256.rbegin(); | ||
| 75 | it != b256.rend(); ++it) | ||
| 76 | { | ||
| 77 | carry += 58 * (*it); | ||
| 78 | *it = (unsigned char)(carry & 0xff); | ||
| 79 | carry >>= 8; | ||
| 80 | } | ||
| 81 | if (carry != 0) | ||
| 82 | return false; | ||
| 83 | } | ||
| 84 | |||
| 85 | size_t zeros = 0; | ||
| 86 | while (zeros < str.size() && str[zeros] == '1') | ||
| 87 | zeros++; | ||
| 88 | std::vector<unsigned char>::iterator it = b256.begin(); | ||
| 89 | while (it != b256.end() && *it == 0) | ||
| 90 | ++it; | ||
| 91 | |||
| 92 | std::vector<unsigned char> full; | ||
| 93 | full.assign(zeros, 0); | ||
| 94 | while (it != b256.end()) | ||
| 95 | full.push_back(*it++); | ||
| 96 | if (full.size() < 4) | ||
| 97 | return false; | ||
| 98 | |||
| 99 | std::vector<unsigned char> payload(full.begin(), full.end() - 4); | ||
| 100 | unsigned char h1[SHA256_DIGEST_LENGTH]; | ||
| 101 | unsigned char h2[SHA256_DIGEST_LENGTH]; | ||
| 102 | SHA256(&payload[0], payload.size(), h1); | ||
| 103 | SHA256(h1, sizeof(h1), h2); | ||
| 104 | if (memcmp(h2, &full[full.size() - 4], 4) != 0) | ||
| 105 | return false; | ||
| 106 | |||
| 107 | out = payload; | ||
| 108 | return true; | ||
| 109 | } | ||
| 110 | |||
| 111 | static bool DecodeXPrvNode(const std::string& xprv, BIP32PrivateNode& nodeOut) | ||
| 112 | { | ||
| 113 | nodeOut.privateKey.clear(); | ||
| 114 | nodeOut.chainCode.clear(); | ||
| 115 | |||
| 116 | std::vector<unsigned char> payload; | ||
| 117 | if (!DecodeBase58Check(xprv, payload)) | ||
| 118 | return false; | ||
| 119 | if (payload.size() != 78) | ||
| 120 | return false; | ||
| 121 | if (payload[0] != 0x04 || payload[1] != 0x88 || | ||
| 122 | payload[2] != 0xad || payload[3] != 0xe4) | ||
| 123 | return false; | ||
| 124 | if (payload[45] != 0x00) | ||
| 125 | return false; | ||
| 126 | |||
| 127 | nodeOut.chainCode.assign(payload.begin() + 13, payload.begin() + 45); | ||
| 128 | nodeOut.privateKey.assign(payload.begin() + 46, payload.end()); | ||
| 129 | return true; | ||
| 130 | } | ||
| 131 | |||
| 60 | int main() | 132 | int main() |
| 61 | { | 133 | { |
| 62 | std::string err; | 134 | std::string err; |
| @@ -124,6 +196,53 @@ int main() | |||
| 124 | "pre-hardened index rejected"); | 196 | "pre-hardened index rejected"); |
| 125 | } | 197 | } |
| 126 | 198 | ||
| 199 | printf("bip32_paths\n"); | ||
| 200 | { | ||
| 201 | std::vector<unsigned char> seed = FromHex("000102030405060708090a0b0c0d0e0f"); | ||
| 202 | BIP32PrivateNode master; | ||
| 203 | CHECK(BIP32MasterFromSeed(seed, master, err), "path test master derives"); | ||
| 204 | |||
| 205 | struct PathVector | ||
| 206 | { | ||
| 207 | const char* name; | ||
| 208 | std::vector<unsigned int> path; | ||
| 209 | const char* xprv; | ||
| 210 | }; | ||
| 211 | |||
| 212 | std::vector<PathVector> vectors; | ||
| 213 | vectors.push_back({"m", | ||
| 214 | {}, | ||
| 215 | "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}); | ||
| 216 | vectors.push_back({"m/0'", | ||
| 217 | {0 | BIP32_HARDENED}, | ||
| 218 | "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"}); | ||
| 219 | vectors.push_back({"m/0'/1", | ||
| 220 | {0 | BIP32_HARDENED, 1}, | ||
| 221 | "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"}); | ||
| 222 | vectors.push_back({"m/0'/1/2'", | ||
| 223 | {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED}, | ||
| 224 | "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"}); | ||
| 225 | vectors.push_back({"m/0'/1/2'/2", | ||
| 226 | {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2}, | ||
| 227 | "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"}); | ||
| 228 | vectors.push_back({"m/0'/1/2'/2/1000000000", | ||
| 229 | {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2, 1000000000}, | ||
| 230 | "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"}); | ||
| 231 | |||
| 232 | for (const PathVector& v : vectors) | ||
| 233 | { | ||
| 234 | BIP32PrivateNode actual; | ||
| 235 | BIP32PrivateNode expected; | ||
| 236 | std::string label = std::string("derives ") + v.name; | ||
| 237 | CHECK(BIP32DerivePath(master, v.path, actual, err), label.c_str()); | ||
| 238 | label = std::string("decodes vector ") + v.name; | ||
| 239 | CHECK(DecodeXPrvNode(v.xprv, expected), label.c_str()); | ||
| 240 | label = std::string("matches vector ") + v.name; | ||
| 241 | CHECK(actual.privateKey == expected.privateKey && | ||
| 242 | actual.chainCode == expected.chainCode, label.c_str()); | ||
| 243 | } | ||
| 244 | } | ||
| 245 | |||
| 127 | printf("\n%s (%d failures)\n", | 246 | printf("\n%s (%d failures)\n", |
| 128 | g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); | 247 | g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); |
| 129 | return g_fail == 0 ? 0 : 1; | 248 | return g_fail == 0 ? 0 : 1; |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 41b4ed2..b449df9 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -25,16 +25,34 @@ | |||
| 25 | // would restore a wallet that looks empty. | 25 | // would restore a wallet that looks empty. |
| 26 | static const int RESTORE_BATCH = 100; | 26 | static const int RESTORE_BATCH = 100; |
| 27 | static const int RESTORE_MAX = 10000; | 27 | static const int RESTORE_MAX = 10000; |
| 28 | // The wallet may have a full unused key pool in front of a change address | ||
| 29 | // created while spending pre-phrase coins. Looking only one empty batch ahead | ||
| 30 | // can stop just before that change output. | ||
| 31 | static const int RESTORE_MIN_SCAN = KEYPOOL_SIZE + RESTORE_BATCH; | ||
| 32 | |||
| 33 | // Quiet batches required before giving up. Three, because the wallet's own | 28 | // Quiet batches required before giving up. Three, because the wallet's own |
| 34 | // bookkeeping can leave a gap of two hundred used-nothing indices between one | 29 | // bookkeeping can leave a gap of two hundred used-nothing indices between one |
| 35 | // used address and the next -- see the note at the stop condition. One was not | 30 | // used address and the next -- see the note at the stop condition. One was not |
| 36 | // enough and cost a real balance in testing. | 31 | // enough and cost a real balance in testing. |
| 37 | static const int RESTORE_QUIET_BATCHES = 3; | 32 | static const int RESTORE_QUIET_BATCHES = 3; |
| 33 | // The wallet may have restored once, scanned a few quiet batches, and then | ||
| 34 | // spent after that. Change starts from wherever that restore stopped, so the | ||
| 35 | // next real change output can be just beyond the old quiet window. The default | ||
| 36 | // restore has to look far enough for that ordinary "restore, then spend, then | ||
| 37 | // restore again" path; explicit -restoredepth can still go deeper. | ||
| 38 | static const int RESTORE_MIN_SCAN = | ||
| 39 | KEYPOOL_SIZE + (RESTORE_QUIET_BATCHES + 1) * RESTORE_BATCH; | ||
| 40 | |||
| 41 | bool RestoreScanReachedDepth(int nSchema, | ||
| 42 | unsigned int nReceiveNext, | ||
| 43 | unsigned int nChangeNext, | ||
| 44 | unsigned int nLegacyNext, | ||
| 45 | int nStopDepth) | ||
| 46 | { | ||
| 47 | if (nStopDepth <= 0) | ||
| 48 | return true; | ||
| 49 | unsigned int nDepth = (unsigned int)nStopDepth; | ||
| 50 | if (nSchema == HD_SCHEMA_BIP44) | ||
| 51 | return nReceiveNext >= nDepth && | ||
| 52 | nChangeNext >= nDepth && | ||
| 53 | nLegacyNext >= nDepth; | ||
| 54 | return nLegacyNext >= nDepth; | ||
| 55 | } | ||
| 38 | 56 | ||
| 39 | int CmdNewPhrase() | 57 | int CmdNewPhrase() |
| 40 | { | 58 | { |
| @@ -114,7 +132,9 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 114 | // Derive forward in batches, scanning after each, until a whole batch turns | 132 | // Derive forward in batches, scanning after each, until a whole batch turns |
| 115 | // up nothing. Every derived key is written to the wallet before the scan, | 133 | // up nothing. Every derived key is written to the wallet before the scan, |
| 116 | // because the scan asks the wallet what belongs to it. | 134 | // because the scan asks the wallet what belongs to it. |
| 117 | int nTotalDerived = (int)nHDNext; | 135 | int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44 |
| 136 | ? (int)(nHDReceiveNext + nHDChangeNext) | ||
| 137 | : (int)nHDNext; | ||
| 118 | int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); | 138 | int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); |
| 119 | int nQuietBatches = 0; | 139 | int nQuietBatches = 0; |
| 120 | while (nTotalDerived < RESTORE_MAX) | 140 | while (nTotalDerived < RESTORE_MAX) |
| @@ -131,17 +151,61 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 131 | nWalletBefore = mapWallet.size(); | 151 | nWalletBefore = mapWallet.size(); |
| 132 | 152 | ||
| 133 | std::string strDeriveError; | 153 | std::string strDeriveError; |
| 134 | for (int i = 0; i < RESTORE_BATCH; i++) | 154 | if (nHDKeySchema == HD_SCHEMA_BIP44) |
| 155 | { | ||
| 156 | for (int i = 0; i < RESTORE_BATCH; i++) | ||
| 157 | { | ||
| 158 | CKey key; | ||
| 159 | if (!DeriveHDKey(nHDReceiveNext, key, strDeriveError)) | ||
| 160 | break; | ||
| 161 | if (!AddKey(key)) | ||
| 162 | break; | ||
| 163 | nHDReceiveNext++; | ||
| 164 | nTotalDerived++; | ||
| 165 | } | ||
| 166 | CWalletDB().WriteHDReceiveNext(nHDReceiveNext); | ||
| 167 | |||
| 168 | for (int i = 0; i < RESTORE_BATCH; i++) | ||
| 169 | { | ||
| 170 | CKey key; | ||
| 171 | if (!DeriveHDChangeKey(nHDChangeNext, key, strDeriveError)) | ||
| 172 | break; | ||
| 173 | if (!AddKey(key)) | ||
| 174 | break; | ||
| 175 | nHDChangeNext++; | ||
| 176 | nTotalDerived++; | ||
| 177 | } | ||
| 178 | CWalletDB().WriteHDChangeNext(nHDChangeNext); | ||
| 179 | |||
| 180 | int nSavedSchema = nHDKeySchema; | ||
| 181 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 182 | for (int i = 0; i < RESTORE_BATCH; i++) | ||
| 183 | { | ||
| 184 | CKey key; | ||
| 185 | if (!DeriveHDKey(nHDNext, key, strDeriveError)) | ||
| 186 | break; | ||
| 187 | if (!AddKey(key)) | ||
| 188 | break; | ||
| 189 | nHDNext++; | ||
| 190 | nTotalDerived++; | ||
| 191 | } | ||
| 192 | nHDKeySchema = nSavedSchema; | ||
| 193 | CWalletDB().WriteHDNext(nHDNext); | ||
| 194 | } | ||
| 195 | else | ||
| 135 | { | 196 | { |
| 136 | CKey key; | 197 | for (int i = 0; i < RESTORE_BATCH; i++) |
| 137 | if (!DeriveHDKey(nHDNext, key, strDeriveError)) | 198 | { |
| 138 | break; | 199 | CKey key; |
| 139 | if (!AddKey(key)) | 200 | if (!DeriveHDKey(nHDNext, key, strDeriveError)) |
| 140 | break; | 201 | break; |
| 141 | nHDNext++; | 202 | if (!AddKey(key)) |
| 142 | nTotalDerived++; | 203 | break; |
| 204 | nHDNext++; | ||
| 205 | nTotalDerived++; | ||
| 206 | } | ||
| 207 | CWalletDB().WriteHDNext(nHDNext); | ||
| 143 | } | 208 | } |
| 144 | CWalletDB().WriteHDNext(nHDNext); | ||
| 145 | 209 | ||
| 146 | ScanForWalletTransactions(pindexGenesisBlock); | 210 | ScanForWalletTransactions(pindexGenesisBlock); |
| 147 | 211 | ||
| @@ -157,11 +221,11 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 157 | } | 221 | } |
| 158 | 222 | ||
| 159 | // Stopping needs more than one quiet batch, because this wallet digs | 223 | // Stopping needs more than one quiet batch, because this wallet digs |
| 160 | // gaps in its own derivation. A restore leaves nHDNext at the depth it | 224 | // gaps in its own derivation. A restore leaves the receive counter at |
| 161 | // scanned, the key pool then derives KEYPOOL_SIZE more, and change | 225 | // the depth it scanned, the key pool then derives KEYPOOL_SIZE more, |
| 162 | // takes the index after that -- so the address used *after* a restore | 226 | // and a later spend can use change after that -- so the address used |
| 163 | // can sit two hundred indices past the last one used before it, with | 227 | // *after* a restore can sit two hundred indices past the last one used |
| 164 | // nothing in between. | 228 | // before it, with nothing in between. |
| 165 | // | 229 | // |
| 166 | // Measured, on a real wallet with real coin: restore, spend once, and | 230 | // Measured, on a real wallet with real coin: restore, spend once, and |
| 167 | // the coins land at indices 201 and 302. Restoring again with the old | 231 | // the coins land at indices 201 and 302. Restoring again with the old |
| @@ -171,7 +235,12 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 171 | nQuietBatches++; | 235 | nQuietBatches++; |
| 172 | else | 236 | else |
| 173 | nQuietBatches = 0; | 237 | nQuietBatches = 0; |
| 174 | if (nQuietBatches >= RESTORE_QUIET_BATCHES && nTotalDerived >= nStopDepth) | 238 | if (nQuietBatches >= RESTORE_QUIET_BATCHES && |
| 239 | RestoreScanReachedDepth(nHDKeySchema, | ||
| 240 | nHDReceiveNext, | ||
| 241 | nHDChangeNext, | ||
| 242 | nHDNext, | ||
| 243 | nStopDepth)) | ||
| 175 | break; | 244 | break; |
| 176 | } | 245 | } |
| 177 | 246 | ||
| @@ -283,7 +352,13 @@ int CmdRecoveryAudit() | |||
| 283 | printf("Wallet recovery audit\n"); | 352 | printf("Wallet recovery audit\n"); |
| 284 | printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); | 353 | printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); |
| 285 | if (audit.fHaveSeed) | 354 | if (audit.fHaveSeed) |
| 355 | { | ||
| 356 | printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); | ||
| 357 | printf(" BIP44 coin type: %u (provisional BITFLASH)\n", audit.nCoinType); | ||
| 286 | printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); | 358 | printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); |
| 359 | printf(" receive/change counters: %u/%u\n", | ||
| 360 | audit.nReceiveNext, audit.nChangeNext); | ||
| 361 | } | ||
| 287 | if (!audit.fDeriveComplete) | 362 | if (!audit.fDeriveComplete) |
| 288 | printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); | 363 | printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); |
| 289 | printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); | 364 | printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); |
diff --git a/src/walletcmd.h b/src/walletcmd.h index 69874e4..32d3eef 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h | |||
| @@ -32,6 +32,15 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 32 | int& nRecoveredRet, | 32 | int& nRecoveredRet, |
| 33 | int& nDerivedRet); | 33 | int& nDerivedRet); |
| 34 | 34 | ||
| 35 | // Restore scans stop only after the requested depth is reached. BIP44 has more | ||
| 36 | // than one branch, so the depth has to be satisfied per branch, not by summing | ||
| 37 | // receive + change + compatibility keys. | ||
| 38 | bool RestoreScanReachedDepth(int nSchema, | ||
| 39 | unsigned int nReceiveNext, | ||
| 40 | unsigned int nChangeNext, | ||
| 41 | unsigned int nLegacyNext, | ||
| 42 | int nStopDepth); | ||
| 43 | |||
| 35 | // Take the next address from the key pool and print it. With a recovery phrase | 44 | // Take the next address from the key pool and print it. With a recovery phrase |
| 36 | // installed the address is derived, so the phrase can bring back whatever is | 45 | // installed the address is derived, so the phrase can bring back whatever is |
| 37 | // paid to it. | 46 | // paid to it. |