diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-03 15:11:55 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-03 15:11:55 -0300 |
| commit | aba252206bf39ac81e6627354062c28fae05b0aa (patch) | |
| tree | 751691f0baaf925c54a2c03f9f02ca87fb29956f /src/selftest.cpp | |
| parent | 1aee805b232a33484e3db1dc8ea4bfb3f585212e (diff) | |
Activate BIP44 wallet derivation (#124)
* Add BIP32 path derivation foundation
* Add HD wallet schema metadata
* Add provisional BIP44 coin type metadata
* Activate BIP44 wallet derivation
* wallet: scan BIP44 restore depth per branch
* wallet: widen default BIP44 restore lookahead
---------
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
Diffstat (limited to 'src/selftest.cpp')
| -rw-r--r-- | src/selftest.cpp | 115 |
1 files changed, 105 insertions, 10 deletions
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, |