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/walletcmd.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/walletcmd.cpp')
| -rw-r--r-- | src/walletcmd.cpp | 117 |
1 files changed, 96 insertions, 21 deletions
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()); |