upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bip32.cpp109
-rw-r--r--src/bip32.h14
-rw-r--r--src/db.cpp50
-rw-r--r--src/db.h27
-rw-r--r--src/gui.cpp5
-rw-r--r--src/main.cpp232
-rw-r--r--src/main.h55
-rw-r--r--src/selftest.cpp115
-rw-r--r--src/test_bip32.cpp119
-rw-r--r--src/walletcmd.cpp117
-rw-r--r--src/walletcmd.h9
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
117static 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
117bool BIP39EntropyToMnemonic(const std::vector<unsigned char>& entropy, 154bool 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
287bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, 324bool 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
404bool 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
430bool 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 @@
16namespace bitflash 16namespace bitflash
17{ 17{
18 18
19static const unsigned int BIP32_HARDENED = 0x80000000U;
20
19struct BIP32PrivateNode 21struct 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
48bool BIP32DeriveChild(const BIP32PrivateNode& parent,
49 unsigned int childNumber,
50 BIP32PrivateNode& childOut,
51 std::string& errorOut);
52
53bool BIP32DerivePath(const BIP32PrivateNode& root,
54 const std::vector<unsigned int>& path,
55 BIP32PrivateNode& nodeOut,
56 std::string& errorOut);
57
46bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, 58bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent,
47 unsigned int childIndex, 59 unsigned int childIndex,
48 BIP32PrivateNode& childOut, 60 BIP32PrivateNode& childOut,
diff --git a/src/db.cpp b/src/db.cpp
index 294920b..b7e7b91 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -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
diff --git a/src/db.h b/src/db.h
index 01b8583..90238bf 100644
--- a/src/db.h
+++ b/src/db.h
@@ -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
184static bool DeriveNextHDWalletKey(vector<unsigned char>& vchPubKeyRet, string& strErrorRet) 184static 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
234static 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;
229vector<unsigned char> vchHDMaster; 297vector<unsigned char> vchHDMaster;
230vector<unsigned char> vchHDChainCode; 298vector<unsigned char> vchHDChainCode;
231unsigned int nHDNext = 0; 299unsigned int nHDNext = 0;
300int nHDKeySchema = HD_SCHEMA_NONE;
301unsigned int nHDReceiveNext = 0;
302unsigned int nHDChangeNext = 0;
303unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL;
304
305string 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
314std::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
321std::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
233static bool ClearKeyPoolRecords(string& strErrorRet) 335static 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
384bool 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());
diff --git a/src/main.h b/src/main.h
index 4886f55..cca2cad 100644
--- a/src/main.h
+++ b/src/main.h
@@ -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.
161extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL) 165extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL)
162extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR) 166extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR)
163extern unsigned int nHDNext; // next child index to derive 167extern unsigned int nHDNext; // next child index to derive
168static const int HD_SCHEMA_NONE = 0;
169static const int HD_SCHEMA_LEGACY = 1; // m/index'
170static const int HD_SCHEMA_BIP44 = 2; // m/44'/coin_type'/account'/change/index
171static const unsigned int HD_BIP44_PURPOSE = 44;
172static const unsigned int HD_BIP44_ACCOUNT = 0;
173static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0;
174static 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.
177static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950;
178extern int nHDKeySchema;
179extern unsigned int nHDReceiveNext; // BIP44 external chain
180extern unsigned int nHDChangeNext; // BIP44 internal chain
181extern unsigned int nHDCoinType; // BIP44 coin_type'
164inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } 182inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; }
183string HDKeySchemaName(int nSchema);
184std::vector<unsigned int> HDLegacyPath(unsigned int nIndex);
185std::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.
170bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); 194bool 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.
174bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); 200bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet);
201bool 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
62static 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
111static 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
60int main() 132int 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.
26static const int RESTORE_BATCH = 100; 26static const int RESTORE_BATCH = 100;
27static const int RESTORE_MAX = 10000; 27static 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.
31static 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.
37static const int RESTORE_QUIET_BATCHES = 3; 32static 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.
38static const int RESTORE_MIN_SCAN =
39 KEYPOOL_SIZE + (RESTORE_QUIET_BATCHES + 1) * RESTORE_BATCH;
40
41bool 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
39int CmdNewPhrase() 57int 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.
38bool 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.