diff options
| author | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-12 14:44:43 -0300 |
|---|---|---|
| committer | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-12 14:44:43 -0300 |
| commit | a109dcd23fd387324e63f265a237723ba1d6c4fa (patch) | |
| tree | badb646bd1113bc21f2078dd7c427dae06ffa992 | |
| parent | bc3de301d9ed0e5fb5e2034be2e066d77cf23804 (diff) | |
wallet: collapse restored legacy hdnext
| -rw-r--r-- | src/selftest.cpp | 28 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 62 | ||||
| -rw-r--r-- | src/walletcmd.h | 9 |
3 files changed, 93 insertions, 6 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp index cca5335..bcbf9d3 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -586,6 +586,34 @@ static int RunWalletHDSelfTest() | |||
| 586 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; | 586 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; |
| 587 | nFail += Check(audit.nLegacyImmatureTx == 1, | 587 | nFail += Check(audit.nLegacyImmatureTx == 1, |
| 588 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; | 588 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; |
| 589 | |||
| 590 | CKey keyLegacyHD11; | ||
| 591 | nSchemaForLegacyAudit = nHDKeySchema; | ||
| 592 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 593 | if (!DeriveHDKey(11, keyLegacyHD11, strError)) | ||
| 594 | throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError); | ||
| 595 | nHDKeySchema = nSchemaForLegacyAudit; | ||
| 596 | if (!AddKey(keyLegacyHD11)) | ||
| 597 | throw std::runtime_error("could not store the legacy compatibility index 11 key"); | ||
| 598 | |||
| 599 | std::map<unsigned int, std::vector<unsigned char> > mapLegacyProbe; | ||
| 600 | mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey(); | ||
| 601 | mapLegacyProbe[11] = keyLegacyHD11.GetPubKey(); | ||
| 602 | |||
| 603 | CWalletTx wtxLegacyHD11; | ||
| 604 | wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG)); | ||
| 605 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 606 | { | ||
| 607 | mapWallet.clear(); | ||
| 608 | mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11; | ||
| 609 | } | ||
| 610 | nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12, | ||
| 611 | "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1; | ||
| 612 | |||
| 613 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 614 | mapWallet.clear(); | ||
| 615 | nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0, | ||
| 616 | "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1; | ||
| 589 | } | 617 | } |
| 590 | catch (const std::exception& e) | 618 | catch (const std::exception& e) |
| 591 | { | 619 | { |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 0ea2251..e8ac4bf 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -54,6 +54,37 @@ bool RestoreScanReachedDepth(int nSchema, | |||
| 54 | return nLegacyNext >= nDepth; | 54 | return nLegacyNext >= nDepth; |
| 55 | } | 55 | } |
| 56 | 56 | ||
| 57 | unsigned int WalletLastUsedPubKeyIndexNext( | ||
| 58 | const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex) | ||
| 59 | { | ||
| 60 | std::map<std::vector<unsigned char>, unsigned int> mapIndexByPubKey; | ||
| 61 | for (std::map<unsigned int, std::vector<unsigned char> >::const_iterator it = | ||
| 62 | mapPubKeysByIndex.begin(); | ||
| 63 | it != mapPubKeysByIndex.end(); ++it) | ||
| 64 | mapIndexByPubKey[it->second] = it->first; | ||
| 65 | |||
| 66 | unsigned int nNext = 0; | ||
| 67 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 68 | { | ||
| 69 | for (map<uint256, CWalletTx>::iterator it = mapWallet.begin(); | ||
| 70 | it != mapWallet.end(); ++it) | ||
| 71 | { | ||
| 72 | CWalletTx& wtx = (*it).second; | ||
| 73 | for (int i = 0; i < (int)wtx.vout.size(); i++) | ||
| 74 | { | ||
| 75 | vector<unsigned char> vchPubKey; | ||
| 76 | if (!ExtractPubKey(wtx.vout[i].scriptPubKey, false, vchPubKey)) | ||
| 77 | continue; | ||
| 78 | std::map<std::vector<unsigned char>, unsigned int>::const_iterator mi = | ||
| 79 | mapIndexByPubKey.find(vchPubKey); | ||
| 80 | if (mi != mapIndexByPubKey.end() && mi->second + 1 > nNext) | ||
| 81 | nNext = mi->second + 1; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | return nNext; | ||
| 86 | } | ||
| 87 | |||
| 57 | int CmdNewPhrase() | 88 | int CmdNewPhrase() |
| 58 | { | 89 | { |
| 59 | AttachTerminal(); | 90 | AttachTerminal(); |
| @@ -132,7 +163,10 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 132 | // Derive forward in batches, scanning after each, until a whole batch turns | 163 | // Derive forward in batches, scanning after each, until a whole batch turns |
| 133 | // up nothing. Every derived key is written to the wallet before the scan, | 164 | // up nothing. Every derived key is written to the wallet before the scan, |
| 134 | // because the scan asks the wallet what belongs to it. | 165 | // because the scan asks the wallet what belongs to it. |
| 135 | int nTotalDerived = nHDKeySchema == HD_SCHEMA_BIP44 | 166 | const bool fRestoreBIP44 = nHDKeySchema == HD_SCHEMA_BIP44; |
| 167 | unsigned int nLegacyScanNext = nHDNext; | ||
| 168 | std::map<unsigned int, vector<unsigned char> > mapLegacyScanPubKeys; | ||
| 169 | int nTotalDerived = fRestoreBIP44 | ||
| 136 | ? (int)(nHDReceiveNext + nHDChangeNext) | 170 | ? (int)(nHDReceiveNext + nHDChangeNext) |
| 137 | : (int)nHDNext; | 171 | : (int)nHDNext; |
| 138 | int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); | 172 | int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); |
| @@ -151,7 +185,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 151 | nWalletBefore = mapWallet.size(); | 185 | nWalletBefore = mapWallet.size(); |
| 152 | 186 | ||
| 153 | std::string strDeriveError; | 187 | std::string strDeriveError; |
| 154 | if (nHDKeySchema == HD_SCHEMA_BIP44) | 188 | if (fRestoreBIP44) |
| 155 | { | 189 | { |
| 156 | for (int i = 0; i < RESTORE_BATCH; i++) | 190 | for (int i = 0; i < RESTORE_BATCH; i++) |
| 157 | { | 191 | { |
| @@ -182,15 +216,15 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 182 | for (int i = 0; i < RESTORE_BATCH; i++) | 216 | for (int i = 0; i < RESTORE_BATCH; i++) |
| 183 | { | 217 | { |
| 184 | CKey key; | 218 | CKey key; |
| 185 | if (!DeriveHDKey(nHDNext, key, strDeriveError)) | 219 | if (!DeriveHDKey(nLegacyScanNext, key, strDeriveError)) |
| 186 | break; | 220 | break; |
| 187 | if (!AddKey(key)) | 221 | if (!AddKey(key)) |
| 188 | break; | 222 | break; |
| 189 | nHDNext++; | 223 | mapLegacyScanPubKeys[nLegacyScanNext] = key.GetPubKey(); |
| 224 | nLegacyScanNext++; | ||
| 190 | nTotalDerived++; | 225 | nTotalDerived++; |
| 191 | } | 226 | } |
| 192 | nHDKeySchema = nSavedSchema; | 227 | nHDKeySchema = nSavedSchema; |
| 193 | CWalletDB().WriteHDNext(nHDNext); | ||
| 194 | } | 228 | } |
| 195 | else | 229 | else |
| 196 | { | 230 | { |
| @@ -209,6 +243,22 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 209 | 243 | ||
| 210 | ScanForWalletTransactions(pindexGenesisBlock); | 244 | ScanForWalletTransactions(pindexGenesisBlock); |
| 211 | 245 | ||
| 246 | if (fRestoreBIP44) | ||
| 247 | { | ||
| 248 | // The legacy branch is scanned only for compatibility with phrases | ||
| 249 | // created before BIP44. Persist the last used legacy index + 1, not | ||
| 250 | // the lookahead depth. Otherwise a restore with no legacy hits can | ||
| 251 | // leave hdnext at RESTORE_MAX and make future compatibility scans | ||
| 252 | // look exhausted even though no legacy key was ever used. | ||
| 253 | unsigned int nLegacyUsedNext = | ||
| 254 | WalletLastUsedPubKeyIndexNext(mapLegacyScanPubKeys); | ||
| 255 | if (nLegacyUsedNext != nHDNext) | ||
| 256 | { | ||
| 257 | nHDNext = nLegacyUsedNext; | ||
| 258 | CWalletDB().WriteHDNext(nHDNext); | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 212 | size_t nWalletAfter = 0; | 262 | size_t nWalletAfter = 0; |
| 213 | CRITICAL_BLOCK(cs_mapWallet) | 263 | CRITICAL_BLOCK(cs_mapWallet) |
| 214 | nWalletAfter = mapWallet.size(); | 264 | nWalletAfter = mapWallet.size(); |
| @@ -239,7 +289,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic, | |||
| 239 | RestoreScanReachedDepth(nHDKeySchema, | 289 | RestoreScanReachedDepth(nHDKeySchema, |
| 240 | nHDReceiveNext, | 290 | nHDReceiveNext, |
| 241 | nHDChangeNext, | 291 | nHDChangeNext, |
| 242 | nHDNext, | 292 | fRestoreBIP44 ? nLegacyScanNext : nHDNext, |
| 243 | nStopDepth)) | 293 | nStopDepth)) |
| 244 | break; | 294 | break; |
| 245 | } | 295 | } |
diff --git a/src/walletcmd.h b/src/walletcmd.h index 04d3fb7..007b8b5 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h | |||
| @@ -7,7 +7,9 @@ | |||
| 7 | #ifndef BITFLASH_WALLETCMD_H | 7 | #ifndef BITFLASH_WALLETCMD_H |
| 8 | #define BITFLASH_WALLETCMD_H | 8 | #define BITFLASH_WALLETCMD_H |
| 9 | 9 | ||
| 10 | #include <map> | ||
| 10 | #include <string> | 11 | #include <string> |
| 12 | #include <vector> | ||
| 11 | 13 | ||
| 12 | // Create a phrase for a wallet that does not have one, install the seed, and | 14 | // Create a phrase for a wallet that does not have one, install the seed, and |
| 13 | // show the words once. Refuses if a phrase already exists. Returns 0 on success. | 15 | // show the words once. Refuses if a phrase already exists. Returns 0 on success. |
| @@ -41,6 +43,13 @@ bool RestoreScanReachedDepth(int nSchema, | |||
| 41 | unsigned int nLegacyNext, | 43 | unsigned int nLegacyNext, |
| 42 | int nStopDepth); | 44 | int nStopDepth); |
| 43 | 45 | ||
| 46 | // Given the pubkeys derived while scanning a compatibility branch, return the | ||
| 47 | // next index after the highest one that actually appears in wallet | ||
| 48 | // transactions. The scan depth can be much larger than this; hdnext must record | ||
| 49 | // use, not how far a restore looked ahead. | ||
| 50 | unsigned int WalletLastUsedPubKeyIndexNext( | ||
| 51 | const std::map<unsigned int, std::vector<unsigned char> >& mapPubKeysByIndex); | ||
| 52 | |||
| 44 | // Take the next address from the key pool and print it. With a recovery phrase | 53 | // Take the next address from the key pool and print it. With a recovery phrase |
| 45 | // installed the address is derived, so the phrase can bring back whatever is | 54 | // installed the address is derived, so the phrase can bring back whatever is |
| 46 | // paid to it. | 55 | // paid to it. |